diff --git a/README.md b/README.md
index 54e285a..745e9d5 100644
--- a/README.md
+++ b/README.md
@@ -14,16 +14,16 @@
kenaito-conf/
├── server/ # Go 管理端(Gin + xorm)
├── web/ # Vue 2 前端
-├── kenaito-conf-go-client/ # Go 客户端 SDK
-├── kenaito-conf-go-client-demo/ # Go 客户端示例
-├── kenaito-conf-springboot2-client/ # Java Spring Boot 2.x 客户端 SDK(JDK 8+)
-├── kenaito-conf-springboot2-client-demo/# Spring Boot 2.x 客户端示例(端口 8081)
-├── kenaito-conf-springboot3-client/ # Java Spring Boot 3 客户端 SDK(JDK 11+,预留升级路径)
-├── kenaito-conf-springboot3-client-demo/# Spring Boot 3 客户端示例(端口 8082,需 JDK 17+)
-├── kenaito-conf-python-client/ # Python 客户端 SDK(Python 3.10+)
-├── kenaito-conf-python-client-demo/ # Python 客户端示例(端口 8082)
-├── kenaito-conf-nodejs-client/ # Node.js 客户端 SDK(Node.js 18+)
-├── kenaito-conf-nodejs-client-demo/ # Node.js 客户端示例(端口 8083)
+├── kenaito-conf-go-client/ # (测试通过)Go 客户端 SDK
+├── kenaito-conf-go-client-demo/ # (测试通过)Go 客户端示例
+├── kenaito-conf-springboot2-client/ # (待测试)Java Spring Boot 2.x 客户端 SDK(JDK 8+)
+├── kenaito-conf-springboot2-client-demo/# (待测试)Spring Boot 2.x 客户端示例(端口 8081)
+├── kenaito-conf-springboot3-client/ # (待测试)Java Spring Boot 3 客户端 SDK(JDK 11+,预留升级路径)
+├── kenaito-conf-springboot3-client-demo/# (待测试)Spring Boot 3 客户端示例(端口 8082,需 JDK 17+)
+├── kenaito-conf-python-client/ # (待测试)Python 客户端 SDK(Python 3.10+)
+├── kenaito-conf-python-client-demo/ # (待测试)Python 客户端示例(端口 8082)
+├── kenaito-conf-nodejs-client/ # (待测试)Node.js 客户端 SDK(Node.js 18+)
+├── kenaito-conf-nodejs-client-demo/ # (待测试)Node.js 客户端示例(端口 8083)
├── kenaito-conf-php-client/ # (待测试)PHP 客户端 SDK(PHP 8.0+)
├── kenaito-conf-php-client-demo/ # (待测试)PHP 客户端示例
├── kenaito-conf-dotnet-client/ # (待测试).NET 客户端 SDK(.NET 6.0+)
diff --git a/kenaito-conf-dotnet-client-demo/Program.cs b/kenaito-conf-dotnet-client-demo/Program.cs
index beac25b..828008e 100644
--- a/kenaito-conf-dotnet-client-demo/Program.cs
+++ b/kenaito-conf-dotnet-client-demo/Program.cs
@@ -18,7 +18,7 @@ var config = new Config
Env = Environment.GetEnvironmentVariable("KENAITO_PROFILE") ?? "daily",
Version = Environment.GetEnvironmentVariable("KENAITO_VERSION") ?? "latest",
Token = Environment.GetEnvironmentVariable("KENAITO_TOKEN") ?? "",
- SyncInterval = int.Parse(Environment.GetEnvironmentVariable("KENAITO_SYNC_INTERVAL") ?? "30"),
+ SyncInterval = int.Parse(Environment.GetEnvironmentVariable("KENAITO_SYNC_INTERVAL") ?? "10"),
CacheDir = Environment.GetEnvironmentVariable("KENAITO_CACHE_DIR") ?? ".",
};
diff --git a/kenaito-conf-go-client-demo/main.go b/kenaito-conf-go-client-demo/main.go
index 40c1b22..2ecd341 100644
--- a/kenaito-conf-go-client-demo/main.go
+++ b/kenaito-conf-go-client-demo/main.go
@@ -17,7 +17,7 @@ func main() {
Env: "daily",
Version: "latest",
//Token: "6f6966fe196572954b176c0e2a97bc399b050c6be31454b8d6b0f7f622b5d6f1",
- SyncInterval: 30, // sync every 30 seconds
+ SyncInterval: 10, // sync every 10 seconds
CacheDir: ".",
}
diff --git a/kenaito-conf-kotlin-client-demo/src/main/kotlin/demo/App.kt b/kenaito-conf-kotlin-client-demo/src/main/kotlin/demo/App.kt
index 980a167..fb6f25b 100644
--- a/kenaito-conf-kotlin-client-demo/src/main/kotlin/demo/App.kt
+++ b/kenaito-conf-kotlin-client-demo/src/main/kotlin/demo/App.kt
@@ -16,7 +16,7 @@ fun main() {
env = System.getenv("KENAITO_PROFILE") ?: "daily",
version = System.getenv("KENAITO_VERSION") ?: "latest",
token = System.getenv("KENAITO_TOKEN") ?: "",
- syncInterval = (System.getenv("KENAITO_SYNC_INTERVAL") ?: "30").toInt(),
+ syncInterval = (System.getenv("KENAITO_SYNC_INTERVAL") ?: "10").toInt(),
cacheDir = System.getenv("KENAITO_CACHE_DIR") ?: ".",
)
diff --git a/kenaito-conf-lua-client-demo/app.lua b/kenaito-conf-lua-client-demo/app.lua
index 1e31199..9e1ec12 100644
--- a/kenaito-conf-lua-client-demo/app.lua
+++ b/kenaito-conf-lua-client-demo/app.lua
@@ -14,7 +14,7 @@ local config = {
env = os.getenv("KENAITO_PROFILE") or "daily",
version = os.getenv("KENAITO_VERSION") or "latest",
token = os.getenv("KENAITO_TOKEN") or "",
- syncInterval = tonumber(os.getenv("KENAITO_SYNC_INTERVAL") or "30"),
+ syncInterval = tonumber(os.getenv("KENAITO_SYNC_INTERVAL") or "10"),
cacheDir = os.getenv("KENAITO_CACHE_DIR") or ".",
}
diff --git a/kenaito-conf-nodejs-client-demo/app.js b/kenaito-conf-nodejs-client-demo/app.js
index 8a23a0f..18ff582 100644
--- a/kenaito-conf-nodejs-client-demo/app.js
+++ b/kenaito-conf-nodejs-client-demo/app.js
@@ -27,7 +27,7 @@ async function createClient() {
env: process.env.KENAITO_PROFILE || 'daily',
version: process.env.KENAITO_VERSION || 'latest',
token: process.env.KENAITO_TOKEN || '',
- syncInterval: parseInt(process.env.KENAITO_SYNC_INTERVAL || '30', 10),
+ syncInterval: parseInt(process.env.KENAITO_SYNC_INTERVAL || '10', 10),
cacheDir: process.env.KENAITO_CACHE_DIR || '.',
});
const c = new ConfigClient(cfg);
diff --git a/kenaito-conf-php-client-demo/app.php b/kenaito-conf-php-client-demo/app.php
index fbeb92a..47178eb 100644
--- a/kenaito-conf-php-client-demo/app.php
+++ b/kenaito-conf-php-client-demo/app.php
@@ -19,7 +19,7 @@ $config = new Config([
'env' => getenv('KENAITO_PROFILE') ?: 'daily',
'version' => getenv('KENAITO_VERSION') ?: 'latest',
'token' => getenv('KENAITO_TOKEN') ?: '',
- 'syncInterval' => (int)(getenv('KENAITO_SYNC_INTERVAL') ?: 30),
+ 'syncInterval' => (int)(getenv('KENAITO_SYNC_INTERVAL') ?: 10),
'cacheDir' => getenv('KENAITO_CACHE_DIR') ?: '.',
]);
diff --git a/kenaito-conf-springboot2-client-demo/pom.xml b/kenaito-conf-springboot2-client-demo/pom.xml
index 0f5d3b5..64f9102 100644
--- a/kenaito-conf-springboot2-client-demo/pom.xml
+++ b/kenaito-conf-springboot2-client-demo/pom.xml
@@ -36,7 +36,7 @@
cn.odboy
kenaito-conf-springboot2-client
- 1.0.0
+ 1.0.3
diff --git a/kenaito-conf-springboot2-client-demo/src/main/java/cn/odboy/demo/ConfigController.java b/kenaito-conf-springboot2-client-demo/src/main/java/cn/odboy/demo/ConfigController.java
index d35c051..014255b 100644
--- a/kenaito-conf-springboot2-client-demo/src/main/java/cn/odboy/demo/ConfigController.java
+++ b/kenaito-conf-springboot2-client-demo/src/main/java/cn/odboy/demo/ConfigController.java
@@ -1,5 +1,6 @@
package cn.odboy.demo;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -25,6 +26,9 @@ public class ConfigController {
@Value("${database.password:unknown}")
private String password;
+ @Autowired
+ private DatabaseProperties databaseProperties;
+
@GetMapping("/")
public Map index() {
Map result = new HashMap<>();
@@ -36,6 +40,17 @@ public class ConfigController {
return result;
}
+ @GetMapping("/configprops")
+ public Map configprops() {
+ Map result = new HashMap<>();
+ result.put("host", databaseProperties.getHost());
+ result.put("port", databaseProperties.getPort());
+ result.put("name", databaseProperties.getName());
+ result.put("user", databaseProperties.getUser());
+ result.put("password", databaseProperties.getPassword());
+ return result;
+ }
+
@GetMapping("/health")
public String health() {
return "OK";
diff --git a/kenaito-conf-springboot2-client-demo/src/main/java/cn/odboy/demo/DatabaseProperties.java b/kenaito-conf-springboot2-client-demo/src/main/java/cn/odboy/demo/DatabaseProperties.java
new file mode 100644
index 0000000..38709f6
--- /dev/null
+++ b/kenaito-conf-springboot2-client-demo/src/main/java/cn/odboy/demo/DatabaseProperties.java
@@ -0,0 +1,62 @@
+package cn.odboy.demo;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * Database configuration properties
+ *
+ * @author odboy
+ * @version 2.0.0
+ * @date 2026-10-16
+ */
+@Component
+@ConfigurationProperties(prefix = "database")
+public class DatabaseProperties {
+
+ private String host;
+ private String port;
+ private String name;
+ private String user;
+ private String password;
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public String getPort() {
+ return port;
+ }
+
+ public void setPort(String port) {
+ this.port = port;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
diff --git a/kenaito-conf-springboot2-client-demo/src/main/resources/application.properties b/kenaito-conf-springboot2-client-demo/src/main/resources/application.properties
index 2a42c88..6e1814f 100644
--- a/kenaito-conf-springboot2-client-demo/src/main/resources/application.properties
+++ b/kenaito-conf-springboot2-client-demo/src/main/resources/application.properties
@@ -11,7 +11,7 @@ kenaito.config.appName=kenaito-springboot-demo
kenaito.config.profile=daily
kenaito.config.version=latest
kenaito.config.token=
-kenaito.config.sync-interval-seconds=30
+kenaito.config.sync-interval-seconds=10
# Local configs (will be overridden by remote if exists)
database.host=192.168.1.1
diff --git a/kenaito-conf-springboot2-client/pom.xml b/kenaito-conf-springboot2-client/pom.xml
index 5b68782..81cbaa8 100644
--- a/kenaito-conf-springboot2-client/pom.xml
+++ b/kenaito-conf-springboot2-client/pom.xml
@@ -3,7 +3,7 @@
4.0.0
cn.odboy
kenaito-conf-springboot2-client
- 1.0.0
+ 1.0.3
jar
diff --git a/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/ConfigcenterContext.java b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/ConfigcenterContext.java
index d37e0ee..8e22527 100644
--- a/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/ConfigcenterContext.java
+++ b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/ConfigcenterContext.java
@@ -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");
}
}
diff --git a/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/InternalConfigChangeListener.java b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/InternalConfigChangeListener.java
index 674fda8..e6c8a46 100644
--- a/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/InternalConfigChangeListener.java
+++ b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/InternalConfigChangeListener.java
@@ -40,14 +40,14 @@ public class InternalConfigChangeListener {
hbScheduledExecutor = Executors.newScheduledThreadPool(1);
final String url = context.getApiBaseUrls()[0] + "/api/fetchChangedConfigs";
- final Map params = new HashMap<>();
+ final Map 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 data = (Map) 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);
}
diff --git a/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/RefreshScopeRegistry.java b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/RefreshScopeRegistry.java
new file mode 100644
index 0000000..c89cfea
--- /dev/null
+++ b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/RefreshScopeRegistry.java
@@ -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 valueFields = new ArrayList<>();
+ private final List 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 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;
+ }
+ }
+}
diff --git a/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoEnvironmentInitializer.java b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoEnvironmentInitializer.java
index 6f86fc6..bb0cdd9 100644
--- a/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoEnvironmentInitializer.java
+++ b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoEnvironmentInitializer.java
@@ -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;
diff --git a/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoRefreshAutoConfiguration.java b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoRefreshAutoConfiguration.java
new file mode 100644
index 0000000..f7054c6
--- /dev/null
+++ b/kenaito-conf-springboot2-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoRefreshAutoConfiguration.java
@@ -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);
+ }
+}
diff --git a/kenaito-conf-springboot2-client/src/main/resources/META-INF/spring.factories b/kenaito-conf-springboot2-client/src/main/resources/META-INF/spring.factories
index cfbe8a8..64e9e34 100644
--- a/kenaito-conf-springboot2-client/src/main/resources/META-INF/spring.factories
+++ b/kenaito-conf-springboot2-client/src/main/resources/META-INF/spring.factories
@@ -1 +1,3 @@
org.springframework.boot.env.EnvironmentPostProcessor=cn.odboy.confcenter.springboot.KenaitoEnvironmentInitializer
+
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.odboy.confcenter.springboot.KenaitoRefreshAutoConfiguration
diff --git a/kenaito-conf-springboot3-client-demo/README.md b/kenaito-conf-springboot3-client-demo/README.md
deleted file mode 100644
index 0725b05..0000000
--- a/kenaito-conf-springboot3-client-demo/README.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# kenaito-conf-springboot3-client-demo
-
-配置中心 Spring Boot 3 客户端示例项目。
-
-## 环境要求
-
-- **JDK 17 或更高版本**(Spring Boot 3 最低要求)
-- Maven 3.6+
-
-## 配置说明
-
-在 `application.properties` 中配置:
-
-```properties
-# 配置中心地址
-kenaito.config.server=http://localhost:8080
-kenaito.config.appName=demo
-kenaito.config.env=dev
-kenaito.config.version=latest
-kenaito.config.profile=dev
-```
-
-## 运行步骤
-
-1. 确保已安装 JDK 17+:
-```bash
-java -version
-```
-
-2. 先安装 kenaito-conf-springboot3-client 到本地 Maven 仓库:
-```bash
-cd E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client
-mvn clean install -DskipTests
-```
-
-3. 运行示例项目:
-```bash
-cd E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client-demo
-mvn spring-boot:run
-```
-
-4. 访问测试:
-```bash
-curl http://localhost:8082/
-```
-
-## 配置优先级
-
-远程配置 > 本地配置
-
-在配置中心添加应用 `demo` 的配置后,远程配置会覆盖本地同名配置。
-
-## 与 Spring Boot 2 Demo 的区别
-
-| 项目 | Spring Boot 版本 | JDK 版本 | 端口 |
-|------|------------------|----------|------|
-| kenaito-conf-springboot2-client-demo | 2.7.18 | 1.8+ | 8081 |
-| kenaito-conf-springboot3-client-demo | 3.2.5 | 17+ | 8082 |
-
-## 注意事项
-
-- 当前环境只有 JDK 11,无法运行此项目
-- 需要安装 JDK 17+ 后才能编译和运行
-- 如果需要在 JDK 11 环境测试,请使用 kenaito-conf-springboot2-client-demo
diff --git a/kenaito-conf-springboot3-client-demo/pom.xml b/kenaito-conf-springboot3-client-demo/pom.xml
deleted file mode 100644
index 30d6454..0000000
--- a/kenaito-conf-springboot3-client-demo/pom.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
- 4.0.0
-
- cn.odboy
- kenaito-conf-springboot3-client-demo
- 1.0.0
- jar
-
- kenaito-conf-springboot3-client-demo
- Demo project for kenaito-conf Spring Boot 3 client
-
-
- org.springframework.boot
- spring-boot-starter-parent
- 3.2.5
-
-
-
-
- 17
- 17
- 17
- UTF-8
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
-
-
- cn.odboy
- kenaito-conf-springboot3-client
- 1.0.0
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
-
-
diff --git a/kenaito-conf-springboot3-client-demo/src/main/java/cn/odboy/demo/ConfigController.java b/kenaito-conf-springboot3-client-demo/src/main/java/cn/odboy/demo/ConfigController.java
deleted file mode 100644
index db18b03..0000000
--- a/kenaito-conf-springboot3-client-demo/src/main/java/cn/odboy/demo/ConfigController.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package cn.odboy.demo;
-
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.HashMap;
-import java.util.Map;
-
-@RestController
-public class ConfigController {
-
- @Value("${app.name:unknown}")
- private String appName;
-
- @Value("${app.version:unknown}")
- private String appVersion;
-
- @Value("${db.url:unknown}")
- private String dbUrl;
-
- @Value("${db.username:unknown}")
- private String dbUsername;
-
- @Value("${custom.config1:unknown}")
- private String customConfig1;
-
- @GetMapping("/")
- public Map index() {
- Map result = new HashMap<>();
- result.put("appName", appName);
- result.put("appVersion", appVersion);
- result.put("dbUrl", dbUrl);
- result.put("dbUsername", dbUsername);
- result.put("customConfig1", customConfig1);
- return result;
- }
-
- @GetMapping("/health")
- public String health() {
- return "OK";
- }
-}
diff --git a/kenaito-conf-springboot3-client-demo/src/main/java/cn/odboy/demo/DemoApplication.java b/kenaito-conf-springboot3-client-demo/src/main/java/cn/odboy/demo/DemoApplication.java
deleted file mode 100644
index a941ddc..0000000
--- a/kenaito-conf-springboot3-client-demo/src/main/java/cn/odboy/demo/DemoApplication.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package cn.odboy.demo;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class DemoApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(DemoApplication.class, args);
- }
-}
diff --git a/kenaito-conf-springboot3-client-demo/src/main/resources/META-INF/spring.factories b/kenaito-conf-springboot3-client-demo/src/main/resources/META-INF/spring.factories
deleted file mode 100644
index a58f71e..0000000
--- a/kenaito-conf-springboot3-client-demo/src/main/resources/META-INF/spring.factories
+++ /dev/null
@@ -1,2 +0,0 @@
-org.springframework.boot.env.EnvironmentPostProcessor=\
-cn.odboy.confcenter.springboot.KenaitoEnvironmentInitializer
diff --git a/kenaito-conf-springboot3-client-demo/src/main/resources/application.properties b/kenaito-conf-springboot3-client-demo/src/main/resources/application.properties
deleted file mode 100644
index a485b1e..0000000
--- a/kenaito-conf-springboot3-client-demo/src/main/resources/application.properties
+++ /dev/null
@@ -1,21 +0,0 @@
-# Server
-server.port=8081
-
-# Application
-app.name=kenaito-conf-springboot3-client-demo
-app.version=1.0.0
-
-# Kenaito Config Center
-kenaito.config.baseUrl=http://localhost:8080
-kenaito.config.appName=kenaito-springboot-demo
-kenaito.config.profile=daily
-kenaito.config.version=latest
-kenaito.config.token=
-kenaito.config.sync-interval-seconds=30
-
-# Local configs (will be overridden by remote if exists)
-database.host=192.168.1.1
-database.port=8888
-database.name=xxxx
-database.user=xxxx
-database.password=xxxxxxxx
diff --git a/kenaito-conf-springboot3-client/README.md b/kenaito-conf-springboot3-client/README.md
deleted file mode 100644
index 5aa329c..0000000
--- a/kenaito-conf-springboot3-client/README.md
+++ /dev/null
@@ -1,107 +0,0 @@
-# kenaito-conf-springboot3-client
-
-配置中心 Spring Boot 3 客户端(当前使用 Spring Boot 2.7.18 + JDK 11 编译)
-
-## 重要说明
-
-**当前状态:**
-- 项目名称为 springboot3-client,但实际使用 Spring Boot 2.7.18 + JDK 11 编译
-- 代码已重构,移除了 mendmix 依赖,使用标准 Java/Spring 工具类
-- 为未来升级到 Spring Boot 3 做好准备
-
-**升级到 Spring Boot 3 的步骤:**
-1. 安装 JDK 17 或更高版本
-2. 修改 `pom.xml`:
- ```xml
- 17
- 17
- 3.2.5
- ```
-3. 修改 Spring Framework 版本:
- ```xml
- 6.1.6
- ```
-4. 重新编译:`mvn clean install`
-
-## 与 springboot2-client 的区别
-
-1. **移除 mendmix 依赖** - 使用标准 Java/Spring 工具类替代
-2. **使用 Java HttpClient** - 替代 mendmix 的 HttpUtils
-3. **使用 Jackson** - 替代 mendmix 的 JsonUtils
-4. **内置加密工具** - 使用 Java 原生 javax.crypto 实现 AES/DES 加密
-5. **更清晰的代码结构** - 便于维护和扩展
-
-## 配置说明
-
-在 `application.properties` 中配置:
-
-```properties
-# 启用配置中心
-kenaito.config.enabled=true
-
-# 配置中心服务地址(支持多个,逗号分隔)
-kenaito.config.baseUrl=http://localhost:8080
-
-# 应用名称
-kenaito.config.appName=myapp
-
-# 环境标识
-kenaito.config.profile=dev
-
-# 版本号
-kenaito.config.version=latest
-
-# 同步间隔(秒)
-kenaito.config.sync-interval-seconds=60
-
-# 认证 token(如果配置中心启用了认证)
-kenaito.config.token=your-token-here
-```
-
-## 使用方式
-
-### Spring Boot 应用
-
-自动生效,无需额外配置。`KenaitoEnvironmentInitializer` 会在 Spring Boot 启动时自动加载远程配置。
-
-### 传统 Spring 应用
-
-在 Spring 配置文件中添加:
-
-```xml
-
-
-
-```
-
-## 安装到本地 Maven 仓库
-
-```bash
-mvn clean install -DskipTests
-```
-
-## 依赖说明
-
-- Spring Boot 2.7.18(optional)
-- Spring Context 5.3.31(optional)
-- Jackson 2.17.0(JSON 处理)
-- SLF4J 2.0.12(日志)
-- Java HttpClient(HTTP 请求,JDK 11+ 内置)
-
-## 升级路径
-
-当你的项目升级到 JDK 17 后,可以修改 pom.xml 中的版本配置,切换到 Spring Boot 3:
-
-```xml
-
- 17
- 17
- 3.2.5
-
-
-
- org.springframework
- spring-context
- 6.1.6
-
-```
diff --git a/kenaito-conf-springboot3-client/pom.xml b/kenaito-conf-springboot3-client/pom.xml
deleted file mode 100644
index c513afb..0000000
--- a/kenaito-conf-springboot3-client/pom.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
- 4.0.0
- cn.odboy
- kenaito-conf-springboot3-client
- 1.0.0
- jar
-
-
- UTF-8
- 11
- 11
- 2.7.18
-
-
-
-
- org.springframework.boot
- spring-boot
- ${spring-boot.version}
- true
-
-
-
- org.springframework
- spring-context
- 5.3.31
- true
-
-
-
-
- org.apache.httpcomponents.client5
- httpclient5
- 5.3.1
-
-
-
-
- com.fasterxml.jackson.core
- jackson-databind
- 2.17.0
-
-
-
-
- org.slf4j
- slf4j-api
- 2.0.12
-
-
-
diff --git a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/ConfigChangeHanlder.java b/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/ConfigChangeHanlder.java
deleted file mode 100644
index 5ff6b53..0000000
--- a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/ConfigChangeHanlder.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package cn.odboy.confcenter;
-
-import java.util.Map;
-
-public interface ConfigChangeHanlder {
- public void onConfigChanged(Map changedConfigs);
-}
diff --git a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/ConfigcenterContext.java b/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/ConfigcenterContext.java
deleted file mode 100644
index c27eba7..0000000
--- a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/ConfigcenterContext.java
+++ /dev/null
@@ -1,500 +0,0 @@
-package cn.odboy.confcenter;
-
-import java.net.InetAddress;
-import java.nio.charset.StandardCharsets;
-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.Map.Entry;
-import java.util.Objects;
-import java.util.Properties;
-import java.util.Set;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.core.env.MapPropertySource;
-import org.springframework.core.env.MutablePropertySources;
-import org.springframework.core.env.PropertiesPropertySource;
-import org.springframework.core.env.StandardEnvironment;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-/**
- * Config Center Context for Spring Boot 3
- *
- * @author odboy
- * @version 2.0.0
- * @date 2026-10-11
- */
-public class ConfigcenterContext {
-
- private final static Logger logger = LoggerFactory.getLogger("cn.odboy");
-
- private static ConfigcenterContext instance = new ConfigcenterContext();
-
- private List sensitiveKeys = new ArrayList<>(Arrays.asList("password","key","secret","token","credentials"));
-
- public static final String MANAGER_PROPERTY_SOURCE = "kenaitoConfigPropertySource";
-
- private static final String IGNORE_PLACEHOLER = "[Ignore]";
-
- private static final String CRYPT_PREFIX = "{Cipher}";
-
- private static final ObjectMapper objectMapper = new ObjectMapper();
-
- private Boolean remoteEnabled;
-
- private final String nodeId;
- private String[] apiBaseUrls;
- private String app;
- private String env;
- private String version;
- private boolean ignoreGlobal;
- private String secret;
- private String globalSecret;
- private String token;
- private boolean remoteFirst = false;
- private boolean isSpringboot;
- private int syncIntervalSeconds = 50;
- private InternalConfigChangeListener configChangeListener;
-
- private List configChangeHanlders;
-
- private boolean processed;
-
- private Properties remoteProperties;
-
- private Properties localProperties = new Properties();
-
- private StandardEnvironment environment;
-
- private ConfigcenterContext() {
- String nodeName;
- try {
- nodeName = InetAddress.getLocalHost().getHostName();
- } catch (Exception e) {
- nodeName = "unknown";
- }
- this.nodeId = nodeName;
- }
-
- public boolean isProcessed() {
- return processed;
- }
-
- public synchronized void init(Properties properties, boolean isSpringboot) {
- if (processed || !isRemoteEnabled()) return;
- if (properties != null) {
- localProperties.putAll(properties);
- }
-
- System.setProperty("client.nodeId", nodeId);
- System.setProperty("springboot", String.valueOf(isSpringboot));
- this.isSpringboot = isSpringboot;
-
- String defaultAppName = getProperty("spring.application.name", "unknown");
- app = getProperty("kenaito.config.appName", defaultAppName);
- if (remoteEnabled == null) {
- remoteEnabled = Boolean.parseBoolean(getProperty("kenaito.config.enabled", "true"));
- }
-
- if (!isRemoteEnabled()) return;
-
- env = getProperty("kenaito.config.profile", "dev");
-
- if (env == null || env.isBlank()) {
- throw new IllegalArgumentException("[kenaito.config.profile] is required");
- }
-
- setApiBaseUrl(getProperty("kenaito.config.baseUrl", getProperty("kenaito.config.baseUrl", "")));
-
- version = getProperty("kenaito.config.version", "latest");
- ignoreGlobal = Boolean.parseBoolean(getProperty("kenaito.config.global-ignore", "false"));
- syncIntervalSeconds = Integer.parseInt(getProperty("kenaito.config.sync-interval-seconds", "60"));
- token = getProperty("kenaito.config.token", "");
-
- System.out.println(String.format("\n=====Configcenter config=====\nappName:%s\nenv:%s\nversion:%s\nremoteEnabled:%s\napiBaseUrls:%s\n=====Configcenter config=====",
- app, env, version, isRemoteEnabled(), toJson(apiBaseUrls)));
- }
-
- public static ConfigcenterContext getInstance() {
- return instance;
- }
-
- public boolean isRemoteEnabled() {
- return remoteEnabled == null || remoteEnabled;
- }
-
- public void setRemoteEnabled(boolean remoteEnabled) {
- this.remoteEnabled = remoteEnabled;
- }
-
- public String[] getApiBaseUrls() {
- return apiBaseUrls;
- }
-
- public void setApiBaseUrl(String apiBaseUrl) {
- if (apiBaseUrl == null || apiBaseUrl.isBlank()) {
- throw new IllegalArgumentException("[kenaito.config.baseUrl] is required");
- }
-
- String[] urls = apiBaseUrl.split(",|;");
- this.apiBaseUrls = new String[urls.length];
-
- for (int i = 0; i < urls.length; i++) {
- if (urls[i].endsWith("/")) {
- this.apiBaseUrls[i] = urls[i].substring(0, urls[i].length() - 1);
- } else {
- this.apiBaseUrls[i] = urls[i];
- }
- }
- }
-
- public String getApp() {
- return app;
- }
-
- public String getEnv() {
- return env;
- }
-
- public String getVersion() {
- return version;
- }
-
- public boolean isIgnoreGlobal() {
- return ignoreGlobal;
- }
-
- public String getSecret() {
- return secret;
- }
-
- public boolean isRemoteFirst() {
- return remoteFirst;
- }
-
- public int getSyncIntervalSeconds() {
- return syncIntervalSeconds;
- }
-
- public String getNodeId() {
- return nodeId;
- }
-
- public boolean isSpringboot() {
- return isSpringboot;
- }
-
- public void setEnvironment(StandardEnvironment environment) {
- this.environment = environment;
- }
-
- public void mergeRemoteProperties(Properties properties) {
- if (!remoteEnabled) return;
- remoteProperties = getAllRemoteProperties();
- if (remoteProperties != null) {
- Set> entrySet = remoteProperties.entrySet();
- for (Entry