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 entry : entrySet) { - if (!isRemoteFirst() && properties.containsKey(entry.getKey())) { - continue; - } - String value = entry.getValue().toString(); - if (IGNORE_PLACEHOLER.equals(value)) continue; - properties.setProperty(entry.getKey().toString(), value); - } - } - - // Sync merged properties into localProperties for change tracking - localProperties.putAll(properties); - - // Close existing listener before creating new one to prevent scheduler leak - if (configChangeListener != null) { - configChangeListener.close(); - } - // Register listener - configChangeListener = new InternalConfigChangeListener(); - printConfigs(properties); - } - - private Properties getAllRemoteProperties() { - if (remoteProperties != null) return remoteProperties; - Properties properties = new Properties(); - - Map map = fetchConfigFromServer(); - if (map == null) { - logger.warn("fetch remote config error and no local cache available, starting with empty remote config"); - return properties; - } - - secret = Objects.toString(map.remove("kenaito.config.encrypt-secret"), null); - globalSecret = Objects.toString(map.remove("kenaito.config.global-encrypt-secret"), null); - remoteFirst = Boolean.parseBoolean(Objects.toString(map.remove("kenaito.config.remote-config-first"), "false")); - - Set keys = map.keySet(); - for (String key : keys) { - Object value = decodeEncryptIfRequire(map.get(key)); - properties.put(key, value); - } - - return properties; - } - - @SuppressWarnings("unchecked") - private Map fetchConfigFromServer() { - Map result = null; - String errorMsg = null; - - for (String apiBaseUrl : apiBaseUrls) { - String url = apiBaseUrl + "/api/fetchAllConfigs"; - Map params = new HashMap<>(); - params.put("appName", app); - params.put("env", env); - params.put("version", version); - params.put("ignoreGlobal", ignoreGlobal); - if (token != null && !token.isEmpty()) { - params.put("token", token); - } - System.out.println("fetch configs url:" + url); - - try { - String jsonString = HttpUtils.postJson(url, objectMapper.writeValueAsString(params)); - if (jsonString != null) { - result = objectMapper.readValue(jsonString, Map.class); - if (result.containsKey("code")) { - errorMsg = result.get("msg").toString(); - System.err.println("fetch error:" + errorMsg); - result = null; - } else { - break; - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - if (result == null) { - System.out.println(">>>>>remote config fetch error, load from local Cache"); - result = LocalCacheUtils.read(); - } else { - LocalCacheUtils.write(result); - } - return result; - } - - private void printConfigs(Properties properties) { - List sortKeys = new ArrayList<>(); - Set> entrySet = properties.entrySet(); - for (Entry entry : entrySet) { - String key = entry.getKey().toString(); - sortKeys.add(key); - } - Collections.sort(sortKeys); - System.out.println("==================final config list start=================="); - String value; - for (String key : sortKeys) { - value = hideSensitive(key, properties.getProperty(key)); - System.out.println(String.format("%s = %s", key, value)); - } - System.out.println("==================final config list end===================="); - } - - public synchronized void updateConfig(Map updateConfig) { - if (!updateConfig.isEmpty()) { - Set keySet = updateConfig.keySet(); - for (String key : keySet) { - String oldValue = localProperties.getProperty(key); - localProperties.setProperty(key, decodeEncryptIfRequire(updateConfig.get(key)).toString()); - - if (environment != null) { - MutablePropertySources propertySources = environment.getPropertySources(); - - synchronized (propertySources) { - MapPropertySource source = null; - if (!propertySources.contains(MANAGER_PROPERTY_SOURCE)) { - source = new MapPropertySource(MANAGER_PROPERTY_SOURCE, new LinkedHashMap<>()); - environment.getPropertySources().addFirst(source); - } else { - Object existingSource = propertySources.get(MANAGER_PROPERTY_SOURCE); - if (existingSource instanceof MapPropertySource) { - source = (MapPropertySource) existingSource; - } - } - - if (source != null) { - Map map = source.getSource(); - Properties properties = new Properties(); - properties.putAll(map); - properties.putAll(updateConfig); - propertySources.replace(source.getName(), new PropertiesPropertySource(source.getName(), properties)); - } - } - } - - logger.info("Config [{}] Change, oldValue:{}, newValue:{}", key, oldValue, updateConfig.get(key)); - } - - if (configChangeHanlders == null) { - configChangeHanlders = new ArrayList<>(); - } - - for (ConfigChangeHanlder handler : configChangeHanlders) { - try { - handler.onConfigChanged(updateConfig); - logger.info("invoke {}.onConfigChanged successed!", handler.getClass().getName()); - } catch (Exception e) { - e.printStackTrace(); - logger.warn("invoke {}.onConfigChanged error, msg:{}", handler.getClass().getName(), e.getMessage()); - } - } - } - } - - public String buildTokenParameter(String url) { - if (token == null || token.isEmpty()) { - return url; - } - return url + (url.contains("?") ? "&" : "?") + "token=" + token; - } - - public void close() { - if (configChangeListener != null) { - configChangeListener.close(); - } - } - - private Object decodeEncryptIfRequire(Object data) { - if (data == null) return ""; - if (data.toString().startsWith(CRYPT_PREFIX)) { - if (secret == null || secret.isBlank()) { - throw new IllegalArgumentException("config[kenaito.config.encrypt-secret] is required"); - } - data = data.toString().replace(CRYPT_PREFIX, ""); - String decryptString; - try { - decryptString = decryptWithAES(secret, data.toString()); - } catch (Exception e) { - decryptString = decryptWithAES(globalSecret, data.toString()); - } - return decryptString; - } - return data; - } - - private static String decryptWithAES(String key, String data) { - try { - String secretKey = md5(key).substring(16); - byte[] encrypted = Base64.getDecoder().decode(data.getBytes(StandardCharsets.UTF_8)); - byte[] decrypted = aesDecrypt(encrypted, secretKey.getBytes(StandardCharsets.UTF_8)); - return new String(decrypted, StandardCharsets.UTF_8); - } catch (Exception e) { - throw new RuntimeException(String.format("解密错误:%s", data)); - } - } - - private String hideSensitive(String key, String origin) { - if (key == null || key.isBlank() || origin == null || origin.isBlank()) return ""; - boolean isSensitive = false; - for (String k : sensitiveKeys) { - if (key.toLowerCase().contains(k)) { - isSensitive = true; - break; - } - } - int length = origin.length(); - if (isSensitive && length > 1) { - return origin.substring(0, length / 2).concat("****"); - } - return origin; - } - - // Helper methods - private String getProperty(String key, String defaultValue) { - String value = localProperties.getProperty(key); - if (value == null || value.isBlank()) { - value = System.getProperty(key); - } - if (value == null || value.isBlank()) { - value = System.getenv(key); - } - return value != null && !value.isBlank() ? value : defaultValue; - } - - private String getProperty(String key) { - return getProperty(key, null); - } - - private static String toJson(Object obj) { - try { - return objectMapper.writeValueAsString(obj); - } catch (Exception e) { - return "[]"; - } - } - - private static String md5(String input) { - try { - java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); - byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); - StringBuilder sb = new StringBuilder(); - for (byte b : digest) { - sb.append(String.format("%02x", b)); - } - return sb.toString(); - } catch (Exception e) { - throw new RuntimeException("MD5 error", e); - } - } - - private static String md5Short(String input) { - if (input == null || input.isBlank()) return ""; - return md5(input).substring(0, 16); - } - - private static String desEncrypt(String key, String data) { - try { - SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "DES"); - Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); - return Base64.getEncoder().encodeToString(encrypted); - } catch (Exception e) { - throw new RuntimeException("DES encrypt error", e); - } - } - - private static byte[] aesDecrypt(byte[] data, byte[] key) throws Exception { - SecretKey secretKey = new SecretKeySpec(key, "AES"); - // Use CBC mode with zero IV to match Go server - IvParameterSpec iv = new IvParameterSpec(new byte[16]); - Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, secretKey, iv); - byte[] decrypted = cipher.doFinal(data); - // Remove PKCS7 padding - int padding = decrypted[decrypted.length - 1] & 0xFF; - if (padding > 0 && padding <= 16) { - byte[] result = new byte[decrypted.length - padding]; - System.arraycopy(decrypted, 0, result, 0, result.length); - return result; - } - return decrypted; - } - - public void addConfigChangeHandler(ConfigChangeHanlder handler) { - if (configChangeHanlders == null) { - configChangeHanlders = new ArrayList<>(); - } - configChangeHanlders.add(handler); - } -} diff --git a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/EnvironmentChangeEvent.java b/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/EnvironmentChangeEvent.java deleted file mode 100644 index a8a825b..0000000 --- a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/EnvironmentChangeEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.odboy.confcenter; - -import java.util.Set; - -import org.springframework.context.ApplicationEvent; - -public class EnvironmentChangeEvent extends ApplicationEvent { - - private static final long serialVersionUID = 1L; - - private Set keys; - - public EnvironmentChangeEvent(Set keys) { - super(keys); - this.keys = keys; - } - - public Set getKeys() { - return keys; - } -} diff --git a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/HttpUtils.java b/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/HttpUtils.java deleted file mode 100644 index 400c984..0000000 --- a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/HttpUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -package cn.odboy.confcenter; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; - -/** - * HTTP Utilities for Spring Boot 3 - * Uses Java's built-in HttpClient - * - * @author odboy - * @version 2.0.0 - * @date 2026-10-11 - */ -public class HttpUtils { - - private static final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) - .build(); - - public static String get(String url) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .GET() - .timeout(Duration.ofSeconds(30)) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() == 200) { - return response.body(); - } - return null; - } - - public static String postJson(String url, String json) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(json)) - .timeout(Duration.ofSeconds(30)) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() == 200) { - return response.body(); - } - return null; - } -} diff --git a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/InternalConfigChangeListener.java b/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/InternalConfigChangeListener.java deleted file mode 100644 index 9211598..0000000 --- a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/InternalConfigChangeListener.java +++ /dev/null @@ -1,82 +0,0 @@ -package cn.odboy.confcenter; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Internal Config Change Listener - * Polls for config changes via HTTP - * - * @author odboy - * @version 2.0.0 - * @date 2026-10-11 - */ -public class InternalConfigChangeListener { - - private final static Logger logger = LoggerFactory.getLogger("cn.odboy.confcenter"); - - private static final String LATEST_FETCH_TIMESTAMP = "latest.fetch.timestamp"; - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private ScheduledExecutorService hbScheduledExecutor; - - public InternalConfigChangeListener() { - ConfigcenterContext context = ConfigcenterContext.getInstance(); - registerHttpListener(context); - } - - public void close() { - if (hbScheduledExecutor != null) { - hbScheduledExecutor.shutdown(); - } - } - - private void registerHttpListener(ConfigcenterContext context) { - hbScheduledExecutor = Executors.newScheduledThreadPool(1); - - final String url = context.getApiBaseUrls()[0] + "/api/fetchChangedConfigs"; - 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", String.valueOf(System.currentTimeMillis())); - - hbScheduledExecutor.scheduleAtFixedRate(() -> { - try { - String safeUrl = context.buildTokenParameter(url); - String response = HttpUtils.postJson(safeUrl, objectMapper.writeValueAsString(params)); - - if (response != null) { - @SuppressWarnings("unchecked") - Map map = objectMapper.readValue(response, Map.class); - if (map.containsKey("data")) { - @SuppressWarnings("unchecked") - Map data = (Map) map.get("data"); - if (data.containsKey(LATEST_FETCH_TIMESTAMP)) { - params.put("lastTime", data.remove(LATEST_FETCH_TIMESTAMP).toString()); - } - if (!data.isEmpty()) { - context.updateConfig(data); - } - } - } - } catch (Exception e) { - logger.error("Error fetching config changes", e); - } - }, context.getSyncIntervalSeconds(), context.getSyncIntervalSeconds(), TimeUnit.SECONDS); - - logger.info("registerHttpUpdaterListener OK intervalSeconds:{}", context.getSyncIntervalSeconds()); - } -} diff --git a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/LocalCacheUtils.java b/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/LocalCacheUtils.java deleted file mode 100644 index c552e14..0000000 --- a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/LocalCacheUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -package cn.odboy.confcenter; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileWriter; -import java.io.InputStreamReader; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Local Cache Utilities - * - * @author odboy - * @version 2.0.0 - * @date 2026-10-11 - */ -public class LocalCacheUtils { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private static String localStorageDir = System.getProperty("kenaito.configcenter.local-storage-dir", - System.getProperty("user.dir")); - - public static void write(Map datas) { - File dir = new File(localStorageDir); - if (!dir.exists()) - dir.mkdirs(); - File file = new File(dir, "config-cache.log"); - try (FileWriter fileWriter = new FileWriter(file, false)) { - fileWriter.write(objectMapper.writeValueAsString(datas)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @SuppressWarnings("unchecked") - public static Map read() { - File dir = new File(localStorageDir); - if (!dir.exists()) - dir.mkdirs(); - File file = new File(dir, "config-cache.log"); - if (!file.exists()) { - return null; - } - try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) { - StringBuilder buffer = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - buffer.append(line); - } - return objectMapper.readValue(buffer.toString(), Map.class); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } -} diff --git a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/spring/CCPropertyPlaceholderConfigurer.java b/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/spring/CCPropertyPlaceholderConfigurer.java deleted file mode 100644 index c7c3afd..0000000 --- a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/spring/CCPropertyPlaceholderConfigurer.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.odboy.confcenter.spring; - -import java.io.IOException; -import java.util.Properties; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.core.env.StandardEnvironment; - -import cn.odboy.confcenter.ConfigcenterContext; - -/** - * Spring Property Placeholder Configurer for non-SpringBoot applications - * - * @author odboy - * @version 2.0.0 - * @date 2026-10-11 - */ -public class CCPropertyPlaceholderConfigurer extends PropertySourcesPlaceholderConfigurer - implements DisposableBean, ApplicationContextAware { - - private boolean setRemoteEnabled = false; - private ConfigcenterContext ccContext = ConfigcenterContext.getInstance(); - - public void setRemoteEnabled(boolean remoteEnabled) { - ccContext.setRemoteEnabled(remoteEnabled); - setRemoteEnabled = true; - } - - @Override - protected Properties mergeProperties() throws IOException { - Properties properties = super.mergeProperties(); - if (!setRemoteEnabled) { - ccContext.setRemoteEnabled(Boolean.parseBoolean(properties.getProperty("kenaito.configcenter.enabled", "true"))); - } - ccContext.init(properties, false); - ccContext.mergeRemoteProperties(properties); - return properties; - } - - @Override - public void destroy() throws Exception { - ccContext.close(); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - StandardEnvironment environment = (StandardEnvironment) applicationContext.getEnvironment(); - ccContext.setEnvironment(environment); - } -} diff --git a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoEnvironmentInitializer.java b/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoEnvironmentInitializer.java deleted file mode 100644 index 0525aa7..0000000 --- a/kenaito-conf-springboot3-client/src/main/java/cn/odboy/confcenter/springboot/KenaitoEnvironmentInitializer.java +++ /dev/null @@ -1,49 +0,0 @@ -package cn.odboy.confcenter.springboot; - -import java.util.Properties; - -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; - -/** - * Spring Boot 3 Environment Post Processor - * Initializes config center before application context is created - * - * @author odboy - * @version 2.0.0 - * @date 2026-10-11 - */ -public class KenaitoEnvironmentInitializer implements EnvironmentPostProcessor { - - private ConfigcenterContext ccContext = ConfigcenterContext.getInstance(); - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - if (ccContext.isRemoteEnabled() && !ccContext.isProcessed()) { - Properties properties = new Properties(); - ccContext.setEnvironment((StandardEnvironment) environment); - // Extract kenaito.config.* properties from Spring Environment - environment.getPropertySources().forEach(ps -> { - if (ps instanceof org.springframework.core.env.EnumerablePropertySource) { - for (String key : ((org.springframework.core.env.EnumerablePropertySource) ps).getPropertyNames()) { - if (key.startsWith("kenaito.config.") || key.equals("spring.application.name")) { - String val = environment.getProperty(key); - if (val != null) { - properties.setProperty(key, val); - } - } - } - } - }); - ccContext.init(properties, true); - ccContext.mergeRemoteProperties(properties); - PropertiesPropertySource propertySource = new PropertiesPropertySource(ConfigcenterContext.MANAGER_PROPERTY_SOURCE, properties); - environment.getPropertySources().addFirst(propertySource); - } - } -} diff --git a/kenaito-conf-springboot3-client/src/main/resources/META-INF/spring.factories b/kenaito-conf-springboot3-client/src/main/resources/META-INF/spring.factories deleted file mode 100644 index a58f71e..0000000 --- a/kenaito-conf-springboot3-client/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/target/classes/META-INF/spring.factories b/kenaito-conf-springboot3-client/target/classes/META-INF/spring.factories deleted file mode 100644 index a58f71e..0000000 --- a/kenaito-conf-springboot3-client/target/classes/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/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/kenaito-conf-springboot3-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst deleted file mode 100644 index 179dfe7..0000000 --- a/kenaito-conf-springboot3-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ /dev/null @@ -1,8 +0,0 @@ -cn\odboy\confcenter\ConfigChangeHanlder.class -cn\odboy\confcenter\springboot\KenaitoEnvironmentInitializer.class -cn\odboy\confcenter\HttpUtils.class -cn\odboy\confcenter\LocalCacheUtils.class -cn\odboy\confcenter\spring\CCPropertyPlaceholderConfigurer.class -cn\odboy\confcenter\ConfigcenterContext.class -cn\odboy\confcenter\EnvironmentChangeEvent.class -cn\odboy\confcenter\InternalConfigChangeListener.class diff --git a/kenaito-conf-springboot3-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/kenaito-conf-springboot3-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst deleted file mode 100644 index c8477a2..0000000 --- a/kenaito-conf-springboot3-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ /dev/null @@ -1,8 +0,0 @@ -E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client\src\main\java\cn\odboy\confcenter\ConfigcenterContext.java -E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client\src\main\java\cn\odboy\confcenter\ConfigChangeHanlder.java -E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client\src\main\java\cn\odboy\confcenter\EnvironmentChangeEvent.java -E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client\src\main\java\cn\odboy\confcenter\HttpUtils.java -E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client\src\main\java\cn\odboy\confcenter\InternalConfigChangeListener.java -E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client\src\main\java\cn\odboy\confcenter\LocalCacheUtils.java -E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client\src\main\java\cn\odboy\confcenter\spring\CCPropertyPlaceholderConfigurer.java -E:\SelfDev\Gitea2026\kenaito-conf\kenaito-conf-springboot3-client\src\main\java\cn\odboy\confcenter\springboot\KenaitoEnvironmentInitializer.java