feat(springboot3): 添加 RefreshScope 自动配置支持
- 新增 KenaitoRefreshAutoConfiguration 类,实现 ApplicationContextAware - 更新 spring.factories,注册 EnableAutoConfiguration - 与 Spring Boot 2 客户端保持一致,支持 @Value 和 @ConfigurationProperties 动态刷新
This commit is contained in:
@@ -14,16 +14,16 @@
|
|||||||
kenaito-conf/
|
kenaito-conf/
|
||||||
├── server/ # Go 管理端(Gin + xorm)
|
├── server/ # Go 管理端(Gin + xorm)
|
||||||
├── web/ # Vue 2 前端
|
├── web/ # Vue 2 前端
|
||||||
├── kenaito-conf-go-client/ # Go 客户端 SDK
|
├── kenaito-conf-go-client/ # (测试通过)Go 客户端 SDK
|
||||||
├── kenaito-conf-go-client-demo/ # Go 客户端示例
|
├── kenaito-conf-go-client-demo/ # (测试通过)Go 客户端示例
|
||||||
├── kenaito-conf-springboot2-client/ # Java Spring Boot 2.x 客户端 SDK(JDK 8+)
|
├── kenaito-conf-springboot2-client/ # (待测试)Java Spring Boot 2.x 客户端 SDK(JDK 8+)
|
||||||
├── kenaito-conf-springboot2-client-demo/# Spring Boot 2.x 客户端示例(端口 8081)
|
├── 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/ # (待测试)Java Spring Boot 3 客户端 SDK(JDK 11+,预留升级路径)
|
||||||
├── kenaito-conf-springboot3-client-demo/# Spring Boot 3 客户端示例(端口 8082,需 JDK 17+)
|
├── kenaito-conf-springboot3-client-demo/# (待测试)Spring Boot 3 客户端示例(端口 8082,需 JDK 17+)
|
||||||
├── kenaito-conf-python-client/ # Python 客户端 SDK(Python 3.10+)
|
├── kenaito-conf-python-client/ # (待测试)Python 客户端 SDK(Python 3.10+)
|
||||||
├── kenaito-conf-python-client-demo/ # Python 客户端示例(端口 8082)
|
├── kenaito-conf-python-client-demo/ # (待测试)Python 客户端示例(端口 8082)
|
||||||
├── kenaito-conf-nodejs-client/ # Node.js 客户端 SDK(Node.js 18+)
|
├── kenaito-conf-nodejs-client/ # (待测试)Node.js 客户端 SDK(Node.js 18+)
|
||||||
├── kenaito-conf-nodejs-client-demo/ # Node.js 客户端示例(端口 8083)
|
├── kenaito-conf-nodejs-client-demo/ # (待测试)Node.js 客户端示例(端口 8083)
|
||||||
├── kenaito-conf-php-client/ # (待测试)PHP 客户端 SDK(PHP 8.0+)
|
├── kenaito-conf-php-client/ # (待测试)PHP 客户端 SDK(PHP 8.0+)
|
||||||
├── kenaito-conf-php-client-demo/ # (待测试)PHP 客户端示例
|
├── kenaito-conf-php-client-demo/ # (待测试)PHP 客户端示例
|
||||||
├── kenaito-conf-dotnet-client/ # (待测试).NET 客户端 SDK(.NET 6.0+)
|
├── kenaito-conf-dotnet-client/ # (待测试).NET 客户端 SDK(.NET 6.0+)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ var config = new Config
|
|||||||
Env = Environment.GetEnvironmentVariable("KENAITO_PROFILE") ?? "daily",
|
Env = Environment.GetEnvironmentVariable("KENAITO_PROFILE") ?? "daily",
|
||||||
Version = Environment.GetEnvironmentVariable("KENAITO_VERSION") ?? "latest",
|
Version = Environment.GetEnvironmentVariable("KENAITO_VERSION") ?? "latest",
|
||||||
Token = Environment.GetEnvironmentVariable("KENAITO_TOKEN") ?? "",
|
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") ?? ".",
|
CacheDir = Environment.GetEnvironmentVariable("KENAITO_CACHE_DIR") ?? ".",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func main() {
|
|||||||
Env: "daily",
|
Env: "daily",
|
||||||
Version: "latest",
|
Version: "latest",
|
||||||
//Token: "6f6966fe196572954b176c0e2a97bc399b050c6be31454b8d6b0f7f622b5d6f1",
|
//Token: "6f6966fe196572954b176c0e2a97bc399b050c6be31454b8d6b0f7f622b5d6f1",
|
||||||
SyncInterval: 30, // sync every 30 seconds
|
SyncInterval: 10, // sync every 10 seconds
|
||||||
CacheDir: ".",
|
CacheDir: ".",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ fun main() {
|
|||||||
env = System.getenv("KENAITO_PROFILE") ?: "daily",
|
env = System.getenv("KENAITO_PROFILE") ?: "daily",
|
||||||
version = System.getenv("KENAITO_VERSION") ?: "latest",
|
version = System.getenv("KENAITO_VERSION") ?: "latest",
|
||||||
token = System.getenv("KENAITO_TOKEN") ?: "",
|
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") ?: ".",
|
cacheDir = System.getenv("KENAITO_CACHE_DIR") ?: ".",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ local config = {
|
|||||||
env = os.getenv("KENAITO_PROFILE") or "daily",
|
env = os.getenv("KENAITO_PROFILE") or "daily",
|
||||||
version = os.getenv("KENAITO_VERSION") or "latest",
|
version = os.getenv("KENAITO_VERSION") or "latest",
|
||||||
token = os.getenv("KENAITO_TOKEN") or "",
|
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 ".",
|
cacheDir = os.getenv("KENAITO_CACHE_DIR") or ".",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async function createClient() {
|
|||||||
env: process.env.KENAITO_PROFILE || 'daily',
|
env: process.env.KENAITO_PROFILE || 'daily',
|
||||||
version: process.env.KENAITO_VERSION || 'latest',
|
version: process.env.KENAITO_VERSION || 'latest',
|
||||||
token: process.env.KENAITO_TOKEN || '',
|
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 || '.',
|
cacheDir: process.env.KENAITO_CACHE_DIR || '.',
|
||||||
});
|
});
|
||||||
const c = new ConfigClient(cfg);
|
const c = new ConfigClient(cfg);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ $config = new Config([
|
|||||||
'env' => getenv('KENAITO_PROFILE') ?: 'daily',
|
'env' => getenv('KENAITO_PROFILE') ?: 'daily',
|
||||||
'version' => getenv('KENAITO_VERSION') ?: 'latest',
|
'version' => getenv('KENAITO_VERSION') ?: 'latest',
|
||||||
'token' => getenv('KENAITO_TOKEN') ?: '',
|
'token' => getenv('KENAITO_TOKEN') ?: '',
|
||||||
'syncInterval' => (int)(getenv('KENAITO_SYNC_INTERVAL') ?: 30),
|
'syncInterval' => (int)(getenv('KENAITO_SYNC_INTERVAL') ?: 10),
|
||||||
'cacheDir' => getenv('KENAITO_CACHE_DIR') ?: '.',
|
'cacheDir' => getenv('KENAITO_CACHE_DIR') ?: '.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.odboy</groupId>
|
<groupId>cn.odboy</groupId>
|
||||||
<artifactId>kenaito-conf-springboot2-client</artifactId>
|
<artifactId>kenaito-conf-springboot2-client</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.0.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
+15
@@ -1,5 +1,6 @@
|
|||||||
package cn.odboy.demo;
|
package cn.odboy.demo;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -25,6 +26,9 @@ public class ConfigController {
|
|||||||
@Value("${database.password:unknown}")
|
@Value("${database.password:unknown}")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DatabaseProperties databaseProperties;
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public Map<String, Object> index() {
|
public Map<String, Object> index() {
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
@@ -36,6 +40,17 @@ public class ConfigController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/configprops")
|
||||||
|
public Map<String, Object> configprops() {
|
||||||
|
Map<String, Object> 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")
|
@GetMapping("/health")
|
||||||
public String health() {
|
public String health() {
|
||||||
return "OK";
|
return "OK";
|
||||||
|
|||||||
+62
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ kenaito.config.appName=kenaito-springboot-demo
|
|||||||
kenaito.config.profile=daily
|
kenaito.config.profile=daily
|
||||||
kenaito.config.version=latest
|
kenaito.config.version=latest
|
||||||
kenaito.config.token=
|
kenaito.config.token=
|
||||||
kenaito.config.sync-interval-seconds=30
|
kenaito.config.sync-interval-seconds=10
|
||||||
|
|
||||||
# Local configs (will be overridden by remote if exists)
|
# Local configs (will be overridden by remote if exists)
|
||||||
database.host=192.168.1.1
|
database.host=192.168.1.1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>cn.odboy</groupId>
|
<groupId>cn.odboy</groupId>
|
||||||
<artifactId>kenaito-conf-springboot2-client</artifactId>
|
<artifactId>kenaito-conf-springboot2-client</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.0.3</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
+42
-12
@@ -4,18 +4,8 @@ import java.io.FileReader;
|
|||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
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.Map.Entry;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
@@ -411,7 +401,7 @@ public class ConfigcenterContext {
|
|||||||
System.out.println("==================final config list start==================");
|
System.out.println("==================final config list start==================");
|
||||||
for (String key : sortKeys) {
|
for (String key : sortKeys) {
|
||||||
String value = hideSensitive(key, properties.getProperty(key));
|
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====================");
|
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());
|
logger.warn("invoke {}.onConfigChanged error, msg:{}", handler.getClass().getName(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh @Value and @ConfigurationProperties fields
|
||||||
|
logger.info("[RefreshScope] Calling refreshDynamicConfigFields()...");
|
||||||
|
refreshDynamicConfigFields();
|
||||||
|
logger.info("[RefreshScope] refreshDynamicConfigFields() completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshDynamicConfigFields() {
|
||||||
|
if (applicationContext != null) {
|
||||||
|
try {
|
||||||
|
logger.info("[RefreshScope] Starting dynamic config field refresh...");
|
||||||
|
StandardEnvironment env = (StandardEnvironment) applicationContext.getEnvironment();
|
||||||
|
// 直接创建 RefreshScopeRegistry 并注册为 BeanPostProcessor
|
||||||
|
RefreshScopeRegistry registry = new RefreshScopeRegistry(env);
|
||||||
|
|
||||||
|
// 遍历所有已注册的 Bean,记录 @Value 和 @ConfigurationProperties 字段
|
||||||
|
String[] beanNames = applicationContext.getBeanDefinitionNames();
|
||||||
|
logger.info("[RefreshScope] Found {} beans in ApplicationContext", beanNames.length);
|
||||||
|
|
||||||
|
int valueFieldCount = 0;
|
||||||
|
int configPropsCount = 0;
|
||||||
|
for (String beanName : beanNames) {
|
||||||
|
Object bean = applicationContext.getBean(beanName);
|
||||||
|
registry.postProcessAfterInitialization(bean, beanName);
|
||||||
|
}
|
||||||
|
|
||||||
|
valueFieldCount = registry.getValueFieldCount();
|
||||||
|
configPropsCount = registry.getConfigPropsCount();
|
||||||
|
logger.info("[RefreshScope] Recorded {} @Value fields and {} @ConfigurationProperties beans", valueFieldCount, configPropsCount);
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
registry.refresh();
|
||||||
|
logger.info("[RefreshScope] RefreshScopeRegistry.refresh() completed, refreshed {} @Value fields and {} @ConfigurationProperties beans",
|
||||||
|
registry.getValueFieldCount(), registry.getConfigPropsCount());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("[RefreshScope] RefreshScopeRegistry.refresh() error, msg:{}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("[RefreshScope] ApplicationContext is null, cannot refresh dynamic config fields");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -40,14 +40,14 @@ public class InternalConfigChangeListener {
|
|||||||
hbScheduledExecutor = Executors.newScheduledThreadPool(1);
|
hbScheduledExecutor = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
final String url = context.getApiBaseUrls()[0] + "/api/fetchChangedConfigs";
|
final String url = context.getApiBaseUrls()[0] + "/api/fetchChangedConfigs";
|
||||||
final Map<String, String> params = new HashMap<>();
|
final Map<String, Object> params = new HashMap<>();
|
||||||
params.put("version", context.getVersion());
|
params.put("version", context.getVersion());
|
||||||
params.put("appName", context.getApp());
|
params.put("appName", context.getApp());
|
||||||
params.put("env", context.getEnv());
|
params.put("env", context.getEnv());
|
||||||
if (context.isIgnoreGlobal()) {
|
if (context.isIgnoreGlobal()) {
|
||||||
params.put("ignoreGlobal", String.valueOf(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() {
|
hbScheduledExecutor.scheduleAtFixedRate(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -61,7 +61,7 @@ public class InternalConfigChangeListener {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> data = (Map<String, Object>) map.get("data");
|
Map<String, Object> data = (Map<String, Object>) map.get("data");
|
||||||
if (data.containsKey(LATEST_FETCH_TIMESTAMP)) {
|
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);
|
if (!data.isEmpty()) context.updateConfig(data);
|
||||||
}
|
}
|
||||||
|
|||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
package cn.odboy.confcenter;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh Scope Registry
|
||||||
|
* Supports dynamic refresh of @Value and @ConfigurationProperties fields
|
||||||
|
*
|
||||||
|
* @author odboy
|
||||||
|
* @version 2.0.0
|
||||||
|
* @date 2026-10-16
|
||||||
|
*/
|
||||||
|
public class RefreshScopeRegistry implements BeanPostProcessor {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RefreshScopeRegistry.class);
|
||||||
|
|
||||||
|
private Environment environment;
|
||||||
|
private final List<ValueFieldHolder> valueFields = new ArrayList<>();
|
||||||
|
private final List<ConfigPropsHolder> configPropsBeans = new ArrayList<>();
|
||||||
|
|
||||||
|
public RefreshScopeRegistry(Environment environment) {
|
||||||
|
this.environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getValueFieldCount() {
|
||||||
|
return valueFields.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConfigPropsCount() {
|
||||||
|
return configPropsBeans.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||||
|
Class<?> clazz = bean.getClass();
|
||||||
|
|
||||||
|
// Record @Value fields
|
||||||
|
for (Field field : clazz.getDeclaredFields()) {
|
||||||
|
if (field.isAnnotationPresent(Value.class)) {
|
||||||
|
valueFields.add(new ValueFieldHolder(bean, field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record @ConfigurationProperties beans
|
||||||
|
if (clazz.isAnnotationPresent(ConfigurationProperties.class)) {
|
||||||
|
configPropsBeans.add(new ConfigPropsHolder(bean));
|
||||||
|
}
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh all @Value fields and @ConfigurationProperties beans
|
||||||
|
*/
|
||||||
|
public void refresh() {
|
||||||
|
refreshValueFields();
|
||||||
|
refreshConfigPropsBeans();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshValueFields() {
|
||||||
|
for (ValueFieldHolder holder : valueFields) {
|
||||||
|
try {
|
||||||
|
holder.refresh(environment);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore refresh errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshConfigPropsBeans() {
|
||||||
|
for (ConfigPropsHolder holder : configPropsBeans) {
|
||||||
|
try {
|
||||||
|
holder.refresh(environment);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore refresh errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holder for @Value field
|
||||||
|
*/
|
||||||
|
private static class ValueFieldHolder {
|
||||||
|
private final Object bean;
|
||||||
|
private final Field field;
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
ValueFieldHolder(Object bean, Field field) {
|
||||||
|
this.bean = bean;
|
||||||
|
this.field = field;
|
||||||
|
this.key = parseKey(field.getAnnotation(Value.class).value());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseKey(String valueAnnotation) {
|
||||||
|
// Extract key from "${database.host:default}" -> "database.host"
|
||||||
|
String cleaned = valueAnnotation.replace("${", "").replace("}", "");
|
||||||
|
int colonIdx = cleaned.indexOf(':');
|
||||||
|
return colonIdx > 0 ? cleaned.substring(0, colonIdx) : cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh(Environment env) throws Exception {
|
||||||
|
String newValue = env.getProperty(key);
|
||||||
|
if (newValue != null) {
|
||||||
|
field.setAccessible(true);
|
||||||
|
Object oldValue = field.get(bean);
|
||||||
|
Object convertedValue = convertType(field.getType(), newValue);
|
||||||
|
field.set(bean, convertedValue);
|
||||||
|
|
||||||
|
// 验证是否设置成功
|
||||||
|
Object verifyValue = field.get(bean);
|
||||||
|
boolean success = (convertedValue != null && convertedValue.equals(verifyValue));
|
||||||
|
logger.info("[RefreshScope] Refreshed @Value field: {}.{} | old='{}' | new='{}' | converted='{}' | verify='{}' | success={}",
|
||||||
|
bean.getClass().getName(), field.getName(), oldValue, newValue, convertedValue, verifyValue, success);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
logger.warn("[RefreshScope] Field refresh failed! bean={}, field={}, fieldType={}, newValueClass={}",
|
||||||
|
bean.getClass().getName(), field.getName(), field.getType(), convertedValue != null ? convertedValue.getClass() : "null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object convertType(Class<?> type, String value) {
|
||||||
|
if (type == String.class) return value;
|
||||||
|
if (type == int.class || type == Integer.class) return Integer.parseInt(value);
|
||||||
|
if (type == long.class || type == Long.class) return Long.parseLong(value);
|
||||||
|
if (type == boolean.class || type == Boolean.class) return Boolean.parseBoolean(value);
|
||||||
|
if (type == double.class || type == Double.class) return Double.parseDouble(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holder for @ConfigurationProperties bean
|
||||||
|
*/
|
||||||
|
private static class ConfigPropsHolder {
|
||||||
|
private final Object bean;
|
||||||
|
|
||||||
|
ConfigPropsHolder(Object bean) {
|
||||||
|
this.bean = bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh(Environment env) {
|
||||||
|
Class<?> clazz = bean.getClass();
|
||||||
|
ConfigurationProperties anno = clazz.getAnnotation(ConfigurationProperties.class);
|
||||||
|
if (anno != null) {
|
||||||
|
String prefix = anno.prefix();
|
||||||
|
bindProperties(bean, prefix, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindProperties(Object bean, String prefix, Environment env) {
|
||||||
|
try {
|
||||||
|
Class<?> clazz = bean.getClass();
|
||||||
|
for (Field field : getAllDeclaredFields(clazz)) {
|
||||||
|
String propertyName = prefix + "." + camelToKebab(field.getName());
|
||||||
|
String value = env.getProperty(propertyName);
|
||||||
|
if (value != null) {
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(bean, convertType(field.getType(), value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field[] getAllDeclaredFields(Class<?> clazz) {
|
||||||
|
List<Field> fields = new ArrayList<>();
|
||||||
|
while (clazz != null && clazz != Object.class) {
|
||||||
|
for (Field field : clazz.getDeclaredFields()) {
|
||||||
|
fields.add(field);
|
||||||
|
}
|
||||||
|
clazz = clazz.getSuperclass();
|
||||||
|
}
|
||||||
|
return fields.toArray(new Field[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String camelToKebab(String camelCase) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (int i = 0; i < camelCase.length(); i++) {
|
||||||
|
char c = camelCase.charAt(i);
|
||||||
|
if (Character.isUpperCase(c)) {
|
||||||
|
if (i > 0) result.append('-');
|
||||||
|
result.append(Character.toLowerCase(c));
|
||||||
|
} else {
|
||||||
|
result.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object convertType(Class<?> type, String value) {
|
||||||
|
if (type == String.class) return value;
|
||||||
|
if (type == int.class || type == Integer.class) return Integer.parseInt(value);
|
||||||
|
if (type == long.class || type == Long.class) return Long.parseLong(value);
|
||||||
|
if (type == boolean.class || type == Boolean.class) return Boolean.parseBoolean(value);
|
||||||
|
if (type == double.class || type == Double.class) return Double.parseDouble(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-1
@@ -6,7 +6,6 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
import org.springframework.boot.env.EnvironmentPostProcessor;
|
||||||
import org.springframework.core.env.ConfigurableEnvironment;
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
import org.springframework.core.env.PropertiesPropertySource;
|
import org.springframework.core.env.PropertiesPropertySource;
|
||||||
import org.springframework.core.env.StandardEnvironment;
|
|
||||||
|
|
||||||
import cn.odboy.confcenter.ConfigcenterContext;
|
import cn.odboy.confcenter.ConfigcenterContext;
|
||||||
|
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package cn.odboy.confcenter.springboot;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import cn.odboy.confcenter.ConfigcenterContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Boot auto configuration for config center refresh support.
|
||||||
|
*
|
||||||
|
* Captures the Spring ApplicationContext into {@link ConfigcenterContext} so that
|
||||||
|
* remote config changes (detected by the polling listener) can update the Spring
|
||||||
|
* Environment property source and refresh {@code @Value} / {@code @ConfigurationProperties}
|
||||||
|
* fields at runtime.
|
||||||
|
*
|
||||||
|
* The non-SpringBoot path sets the context via CCPropertyPlaceholderConfigurer;
|
||||||
|
* this class provides the equivalent wiring for Spring Boot applications.
|
||||||
|
*
|
||||||
|
* @author odboy
|
||||||
|
* @version 2.0.0
|
||||||
|
* @date 2026-10-16
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class KenaitoRefreshAutoConfiguration implements ApplicationContextAware {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
ConfigcenterContext.setApplicationContext(applicationContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
org.springframework.boot.env.EnvironmentPostProcessor=cn.odboy.confcenter.springboot.KenaitoEnvironmentInitializer
|
org.springframework.boot.env.EnvironmentPostProcessor=cn.odboy.confcenter.springboot.KenaitoEnvironmentInitializer
|
||||||
|
|
||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.odboy.confcenter.springboot.KenaitoRefreshAutoConfiguration
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<groupId>cn.odboy</groupId>
|
|
||||||
<artifactId>kenaito-conf-springboot3-client-demo</artifactId>
|
|
||||||
<version>1.0.0</version>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<name>kenaito-conf-springboot3-client-demo</name>
|
|
||||||
<description>Demo project for kenaito-conf Spring Boot 3 client</description>
|
|
||||||
|
|
||||||
<parent>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
|
||||||
<version>3.2.5</version>
|
|
||||||
<relativePath/>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<java.version>17</java.version>
|
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- kenaito-conf springboot3 client -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.odboy</groupId>
|
|
||||||
<artifactId>kenaito-conf-springboot3-client</artifactId>
|
|
||||||
<version>1.0.0</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
||||||
-43
@@ -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<String, Object> index() {
|
|
||||||
Map<String, Object> 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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
|
||||||
cn.odboy.confcenter.springboot.KenaitoEnvironmentInitializer
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
|
||||||
<spring-boot.version>3.2.5</spring-boot.version>
|
|
||||||
```
|
|
||||||
3. 修改 Spring Framework 版本:
|
|
||||||
```xml
|
|
||||||
<version>6.1.6</version>
|
|
||||||
```
|
|
||||||
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
|
|
||||||
<bean class="cn.odboy.confcenter.spring.CCPropertyPlaceholderConfigurer">
|
|
||||||
<property name="remoteEnabled" value="true"/>
|
|
||||||
</bean>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安装到本地 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
|
|
||||||
<properties>
|
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
|
||||||
<spring-boot.version>3.2.5</spring-boot.version>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework</groupId>
|
|
||||||
<artifactId>spring-context</artifactId>
|
|
||||||
<version>6.1.6</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<groupId>cn.odboy</groupId>
|
|
||||||
<artifactId>kenaito-conf-springboot3-client</artifactId>
|
|
||||||
<version>1.0.0</version>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<maven.compiler.source>11</maven.compiler.source>
|
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
|
||||||
<spring-boot.version>2.7.18</spring-boot.version>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot</artifactId>
|
|
||||||
<version>${spring-boot.version}</version>
|
|
||||||
<optional>true</optional>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework</groupId>
|
|
||||||
<artifactId>spring-context</artifactId>
|
|
||||||
<version>5.3.31</version>
|
|
||||||
<optional>true</optional>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- HTTP Client -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
|
||||||
<artifactId>httpclient5</artifactId>
|
|
||||||
<version>5.3.1</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- JSON Processing -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
|
||||||
<artifactId>jackson-databind</artifactId>
|
|
||||||
<version>2.17.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Logging -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>slf4j-api</artifactId>
|
|
||||||
<version>2.0.12</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
package cn.odboy.confcenter;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public interface ConfigChangeHanlder {
|
|
||||||
public void onConfigChanged(Map<String, Object> changedConfigs);
|
|
||||||
}
|
|
||||||
-500
@@ -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<String> 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<ConfigChangeHanlder> 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<Entry<Object, Object>> entrySet = remoteProperties.entrySet();
|
|
||||||
for (Entry<Object, Object> 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<String, Object> 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<String> keys = map.keySet();
|
|
||||||
for (String key : keys) {
|
|
||||||
Object value = decodeEncryptIfRequire(map.get(key));
|
|
||||||
properties.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Map<String, Object> fetchConfigFromServer() {
|
|
||||||
Map<String, Object> result = null;
|
|
||||||
String errorMsg = null;
|
|
||||||
|
|
||||||
for (String apiBaseUrl : apiBaseUrls) {
|
|
||||||
String url = apiBaseUrl + "/api/fetchAllConfigs";
|
|
||||||
Map<String, Object> 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<String> sortKeys = new ArrayList<>();
|
|
||||||
Set<Entry<Object, Object>> entrySet = properties.entrySet();
|
|
||||||
for (Entry<Object, Object> 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<String, Object> updateConfig) {
|
|
||||||
if (!updateConfig.isEmpty()) {
|
|
||||||
Set<String> 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<String, Object> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-21
@@ -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<String> keys;
|
|
||||||
|
|
||||||
public EnvironmentChangeEvent(Set<String> keys) {
|
|
||||||
super(keys);
|
|
||||||
this.keys = keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getKeys() {
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String> 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<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
return response.body();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-82
@@ -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<String, String> 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<String, Object> map = objectMapper.readValue(response, Map.class);
|
|
||||||
if (map.containsKey("data")) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> data = (Map<String, Object>) map.get("data");
|
|
||||||
if (data.containsKey(LATEST_FETCH_TIMESTAMP)) {
|
|
||||||
params.put("lastTime", data.remove(LATEST_FETCH_TIMESTAMP).toString());
|
|
||||||
}
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-59
@@ -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<String, Object> 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<String, Object> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-54
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-49
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
|
||||||
cn.odboy.confcenter.springboot.KenaitoEnvironmentInitializer
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
|
||||||
cn.odboy.confcenter.springboot.KenaitoEnvironmentInitializer
|
|
||||||
-8
@@ -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
|
|
||||||
-8
@@ -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
|
|
||||||
Reference in New Issue
Block a user