refactor: 配置刷新优化和加密功能移除

- refactor(springboot2): 简化配置刷新逻辑,移除 EnvironmentChangeEvent 包装
  - ConfigChangeHanlder 直接接收 Map<String,String> 参数
  - 移除 EnvironmentChangeEvent 事件类
  - 优化 token 参数构建和敏感信息脱敏处理
- refactor(server): 移除配置加密功能
  - 删除 CryptoConfig 结构体和相关配置项
  - 移除 application-dev.properties 中的加密配置
  - 项目不再使用配置加密功能
- docs: 更新 README 和模块文档
- fix: 修复 Node.js 客户端配置缓存
This commit is contained in:
2026-03-25 21:00:59 +08:00
parent c2e599097e
commit 751a0f770f
6 changed files with 448 additions and 571 deletions
-1
View File
@@ -64,7 +64,6 @@ database.user=root
database.password=your_password database.password=your_password
jwt.secret=your-jwt-secret jwt.secret=your-jwt-secret
jwt.expireHours=24 jwt.expireHours=24
crypto.secretKey=your-crypto-key
``` ```
启动服务(首次启动自动建表并初始化默认数据): 启动服务(首次启动自动建表并初始化默认数据):
@@ -1,5 +1,4 @@
{ {
"crypto.secretKey": "k9#mP2$vL7qR4xN8",
"database.host": "127.0.0.1", "database.host": "127.0.0.1",
"database.name": "kenaito_conf", "database.name": "kenaito_conf",
"database.password": "1234567", "database.password": "1234567",
@@ -4,5 +4,5 @@ import java.util.Map;
public interface ConfigChangeHanlder { public interface ConfigChangeHanlder {
public void onConfigChanged(Map<String, Object> changedConfigs); void onConfigChanged(Map<String, Object> changedConfigs);
} }
@@ -7,12 +7,6 @@ import java.security.MessageDigest;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -34,535 +28,451 @@ import com.alibaba.fastjson2.JSON;
*/ */
public class ConfigcenterContext { public class ConfigcenterContext {
private final static Logger logger = LoggerFactory.getLogger("cn.odboy"); private final static Logger logger = LoggerFactory.getLogger("cn.odboy");
private static ConfigcenterContext instance = new ConfigcenterContext(); private static ConfigcenterContext instance = new ConfigcenterContext();
private List<String> sensitiveKeys = new ArrayList<>(Arrays.asList("password", "key", "secret", "token", "credentials")); private List<String> sensitiveKeys = new ArrayList<>(Arrays.asList("password", "key", "secret", "token", "credentials"));
public static final String MANAGER_PROPERTY_SOURCE = "kenaitoConfigPropertySource"; public static final String MANAGER_PROPERTY_SOURCE = "kenaitoConfigPropertySource";
private static final String IGNORE_PLACEHOLER = "[Ignore]"; private static final String IGNORE_PLACEHOLER = "[Ignore]";
private static final String CRYPT_PREFIX = "{Cipher}"; private static final String CRYPT_PREFIX = "{Cipher}";
// Local property storage (replaces ResourceUtils) // Local property storage (replaces ResourceUtils)
private static final Map<String, String> localProps = new ConcurrentHashMap<>(); private static final Map<String, String> localProps = new ConcurrentHashMap<>();
// Spring ApplicationContext (replaces InstanceFactory) // Spring ApplicationContext (replaces InstanceFactory)
private static ApplicationContext applicationContext; private static ApplicationContext applicationContext;
private Boolean remoteEnabled; private Boolean remoteEnabled;
private final String nodeId; private final String nodeId;
private String[] apiBaseUrls; private String[] apiBaseUrls;
private String app; private String app;
private String env; private String env;
private String version; private String version;
private boolean ignoreGlobal; private boolean ignoreGlobal;
private String secret; private String secret;
private String globalSecret; private String globalSecret;
private String token; private String token;
private boolean remoteFirst = false; private boolean remoteFirst = false;
private boolean isSpringboot; private boolean isSpringboot;
private int syncIntervalSeconds = 50; private int syncIntervalSeconds = 50;
private InternalConfigChangeListener configChangeListener; private InternalConfigChangeListener configChangeListener;
private List<ConfigChangeHanlder> configChangeHanlders; private List<ConfigChangeHanlder> configChangeHanlders;
private boolean processed; private boolean processed;
private Properties remoteProperties; private Properties remoteProperties;
private ConfigcenterContext() { private ConfigcenterContext() {
String name; String name;
try { try {
name = InetAddress.getLocalHost().getHostName(); name = InetAddress.getLocalHost().getHostName();
} catch (Exception e) { } catch (Exception e) {
name = "unknown"; name = "unknown";
} }
this.nodeId = name; this.nodeId = name;
} }
public static ConfigcenterContext getInstance() { public static ConfigcenterContext getInstance() {
return instance; return instance;
} }
public static void setApplicationContext(ApplicationContext ctx) { public static void setApplicationContext(ApplicationContext ctx) {
applicationContext = ctx; applicationContext = ctx;
} }
public static ApplicationContext getApplicationContext() { public static ApplicationContext getApplicationContext() {
return applicationContext; return applicationContext;
} }
// ==================== Property Utils (replaces ResourceUtils) ==================== // ==================== Property Utils (replaces ResourceUtils) ====================
public static void mergeProperties(Properties props) { public static void mergeProperties(Properties props) {
if (props != null) { if (props != null) {
for (Entry<Object, Object> entry : props.entrySet()) { for (Entry<Object, Object> entry : props.entrySet()) {
localProps.put(entry.getKey().toString(), entry.getValue().toString()); localProps.put(entry.getKey().toString(), entry.getValue().toString());
} }
} }
} }
public static void addProperty(String key, String value) { public static void addProperty(String key, String value) {
localProps.put(key, value); localProps.put(key, value);
} }
public static String getProperty(String key) { public static String getProperty(String key) {
String val = localProps.get(key); String val = localProps.get(key);
if (val == null) val = System.getProperty(key); if (val == null) val = System.getProperty(key);
if (val == null) val = System.getenv(key); if (val == null) val = System.getenv(key);
return val; return val;
} }
public static String getProperty(String key, String defaultValue) { public static String getProperty(String key, String defaultValue) {
String val = getProperty(key); String val = getProperty(key);
return val != null && !val.isEmpty() ? val : defaultValue; return val != null && !val.isEmpty() ? val : defaultValue;
} }
public static boolean getBoolean(String key, boolean defaultValue) { public static boolean getBoolean(String key, boolean defaultValue) {
String val = getProperty(key); String val = getProperty(key);
if (val == null || val.isEmpty()) return defaultValue; if (val == null || val.isEmpty()) return defaultValue;
return Boolean.parseBoolean(val); return Boolean.parseBoolean(val);
} }
public static int getInt(String key, int defaultValue) { public static int getInt(String key, int defaultValue) {
String val = getProperty(key); String val = getProperty(key);
if (val == null || val.isEmpty()) return defaultValue; if (val == null || val.isEmpty()) return defaultValue;
try { try {
return Integer.parseInt(val); return Integer.parseInt(val);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
return defaultValue; return defaultValue;
} }
} }
public static String getAnyProperty(String... keys) { public static String getAnyProperty(String... keys) {
for (String key : keys) { for (String key : keys) {
String val = getProperty(key); String val = getProperty(key);
if (val != null && !val.isEmpty()) return val; if (val != null && !val.isEmpty()) return val;
} }
return null; return null;
} }
// ==================== Crypto Utils (replaces mendmix AES/DES/Base64/DigestUtils) ==================== // ==================== HTTP Utils (replaces mendmix HttpUtils) ====================
public static String md5(String input) { public static String httpGet(String url) throws Exception {
try { return HttpClientUtils.get(url);
MessageDigest md = MessageDigest.getInstance("MD5"); }
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(); public static String httpPostJson(String url, String json) throws Exception {
for (byte b : digest) { return HttpClientUtils.postJson(url, json);
sb.append(String.format("%02x", b)); }
}
return sb.toString(); // ==================== Core Methods ====================
} catch (Exception e) {
throw new RuntimeException("MD5 error", e); public boolean isProcessed() {
} return processed;
} }
public static String md5Short(String input) { public synchronized void init(Properties properties, boolean isSpringboot) {
if (input == null || input.isEmpty()) return ""; if (processed || !isRemoteEnabled()) return;
return md5(input).substring(0, 16); if (properties != null) mergeProperties(properties);
}
System.setProperty("client.nodeId", nodeId);
private static String desEncrypt(String key, String data) { System.setProperty("springboot", String.valueOf(isSpringboot));
try { this.isSpringboot = isSpringboot;
SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "DES");
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); String defaultAppName = getProperty("spring.application.name");
cipher.init(Cipher.ENCRYPT_MODE, secretKey); app = getProperty("kenaito.config.appName", defaultAppName);
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); if (remoteEnabled == null) remoteEnabled = getBoolean("kenaito.config.enabled", true);
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) { if (!isRemoteEnabled()) return;
throw new RuntimeException("DES encrypt error", e);
} env = getProperty("kenaito.config.profile", "dev");
} Validate.notBlank(env, "[kenaito.config.profile] is required");
private static byte[] aesDecrypt(byte[] data, byte[] key) throws Exception { setApiBaseUrl(getAnyProperty("kenaito.config.baseUrl", "kenaito.config.baseUrl"));
SecretKey secretKey = new SecretKeySpec(key, "AES");
// Use CBC mode with zero IV to match Go server version = getProperty("kenaito.config.version", "latest");
IvParameterSpec iv = new IvParameterSpec(new byte[16]); ignoreGlobal = getBoolean("kenaito.config.global-ignore", false);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); syncIntervalSeconds = getInt("kenaito.config.sync-interval-seconds", 60);
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv); token = getProperty("kenaito.config.token");
byte[] decrypted = cipher.doFinal(data);
// Remove PKCS7 padding logger.info("\n=====Configcenter config=====\nappName:{}\nenv:{}\nversion:{}\nremoteEnabled:{}\napiBaseUrls:{}\n=====Configcenter config=====", app, env, version, isRemoteEnabled(), JSON.toJSONString(apiBaseUrls));
int padding = decrypted[decrypted.length - 1] & 0xFF; }
if (padding > 0 && padding <= 16) {
byte[] result = new byte[decrypted.length - padding]; public boolean isRemoteEnabled() {
System.arraycopy(decrypted, 0, result, 0, result.length); return remoteEnabled == null || remoteEnabled;
return result; }
}
return decrypted; public void setRemoteEnabled(boolean remoteEnabled) {
} this.remoteEnabled = remoteEnabled;
}
private static String decryptWithAES(String key, String data) {
try { public String[] getApiBaseUrls() {
String secretKey = md5(key).substring(16); return apiBaseUrls;
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); public void setApiBaseUrl(String apiBaseUrl) {
} catch (Exception e) { Validate.notBlank(apiBaseUrl, "[kenaito.config.baseUrl] is required");
throw new RuntimeException(String.format("解密错误:%s", data)); String[] urls = apiBaseUrl.split(",|;");
} this.apiBaseUrls = new String[urls.length];
} for (int i = 0; i < urls.length; i++) {
if (urls[i].endsWith("/")) {
// ==================== HTTP Utils (replaces mendmix HttpUtils) ==================== this.apiBaseUrls[i] = urls[i].substring(0, urls[i].length() - 1);
} else {
public static String httpGet(String url) throws Exception { this.apiBaseUrls[i] = urls[i];
return HttpClientUtils.get(url); }
} }
}
public static String httpPostJson(String url, String json) throws Exception {
return HttpClientUtils.postJson(url, json); public String getApp() {
} return app;
}
// ==================== Core Methods ====================
public String getEnv() {
public boolean isProcessed() { return env;
return processed; }
}
public String getVersion() {
public synchronized void init(Properties properties, boolean isSpringboot) { return version;
if (processed || !isRemoteEnabled()) return; }
if (properties != null) mergeProperties(properties);
public boolean isIgnoreGlobal() {
System.setProperty("client.nodeId", nodeId); return ignoreGlobal;
System.setProperty("springboot", String.valueOf(isSpringboot)); }
this.isSpringboot = isSpringboot;
public boolean isRemoteFirst() {
String defaultAppName = getProperty("spring.application.name"); return remoteFirst;
app = getProperty("kenaito.config.appName", defaultAppName); }
if (remoteEnabled == null) remoteEnabled = getBoolean("kenaito.config.enabled", true);
public int getSyncIntervalSeconds() {
if (!isRemoteEnabled()) return; return syncIntervalSeconds;
}
env = getProperty("kenaito.config.profile", "dev");
Validate.notBlank(env, "[kenaito.config.profile] is required"); public void mergeRemoteProperties(Properties properties) {
if (!remoteEnabled) return;
setApiBaseUrl(getAnyProperty("kenaito.config.baseUrl", "kenaito.config.baseUrl")); remoteProperties = getAllRemoteProperties();
if (remoteProperties != null) {
version = getProperty("kenaito.config.version", "latest"); Set<Entry<Object, Object>> entrySet = remoteProperties.entrySet();
ignoreGlobal = getBoolean("kenaito.config.global-ignore", false); for (Entry<Object, Object> entry : entrySet) {
syncIntervalSeconds = getInt("kenaito.config.sync-interval-seconds", 60); if (!isRemoteFirst() && properties.containsKey(entry.getKey())) {
token = getProperty("kenaito.config.token"); continue;
}
logger.info("\n=====Configcenter config=====\nappName:{}\nenv:{}\nversion:{}\nremoteEnabled:{}\napiBaseUrls:{}\n=====Configcenter config=====", String value = entry.getValue().toString();
app, env, version, isRemoteEnabled(), JSON.toJSONString(apiBaseUrls)); if (IGNORE_PLACEHOLER.equals(value)) continue;
} properties.setProperty(entry.getKey().toString(), value);
}
public boolean isRemoteEnabled() { }
return remoteEnabled == null || remoteEnabled;
} // 替换本地变量占位符
Set<Entry<Object, Object>> entrySet = properties.entrySet();
public void setRemoteEnabled(boolean remoteEnabled) { for (Entry<Object, Object> entry : entrySet) {
this.remoteEnabled = remoteEnabled; String key = entry.getKey().toString();
} String value = entry.getValue().toString();
addProperty(key, value);
public String[] getApiBaseUrls() { }
return apiBaseUrls;
} // 加载本地测试配置
String path = System.getProperty("localtest.config.location");
public void setApiBaseUrl(String apiBaseUrl) { if (StringUtils.isNotBlank(path)) {
Validate.notBlank(apiBaseUrl, "[kenaito.config.baseUrl] is required"); try (FileReader fileReader = new FileReader(path)) {
String[] urls = apiBaseUrl.split(",|;"); Properties localProperties = new Properties();
this.apiBaseUrls = new String[urls.length]; localProperties.load(fileReader);
for (int i = 0; i < urls.length; i++) { if (!localProperties.isEmpty()) {
if (urls[i].endsWith("/")) { properties.putAll(localProperties);
this.apiBaseUrls[i] = urls[i].substring(0, urls[i].length() - 1); }
} else { } catch (Exception e) {
this.apiBaseUrls[i] = urls[i]; logger.warn("Failed to load local test config from {}: {}", path, e.getMessage());
} }
} }
}
// Close existing listener before creating new one
public String getApp() { return app; } if (configChangeListener != null) {
public String getEnv() { return env; } configChangeListener.close();
public String getVersion() { return version; } }
public boolean isIgnoreGlobal() { return ignoreGlobal; } // register listener
public String getSecret() { return secret; } configChangeListener = new InternalConfigChangeListener();
public boolean isRemoteFirst() { return remoteFirst; } printConfigs(properties);
public int getSyncIntervalSeconds() { return syncIntervalSeconds; } }
public String getNodeId() { return nodeId; }
public boolean isSpringboot() { return isSpringboot; } private Properties getAllRemoteProperties() {
if (remoteProperties != null) return remoteProperties;
public void mergeRemoteProperties(Properties properties) { Properties properties = new Properties();
if (!remoteEnabled) return;
remoteProperties = getAllRemoteProperties(); Map<String, Object> map = fetchConfigFromServer();
if (remoteProperties != null) { if (map == null) {
Set<Entry<Object, Object>> entrySet = remoteProperties.entrySet(); throw new RuntimeException("fetch remote config error!");
for (Entry<Object, Object> entry : entrySet) { }
if (!isRemoteFirst() && properties.containsKey(entry.getKey())) {
continue; // 解密密匙
} secret = Objects.toString(map.remove("kenaito.config.encrypt-secret"), null);
String value = entry.getValue().toString(); globalSecret = Objects.toString(map.remove("kenaito.config.global-encrypt-secret"), null);
if (IGNORE_PLACEHOLER.equals(value)) continue; remoteFirst = Boolean.parseBoolean(Objects.toString(map.remove("kenaito.config.remote-config-first"), "false"));
properties.setProperty(entry.getKey().toString(), value);
} Set<String> keys = map.keySet();
} for (String key : keys) {
Object value = map.get(key);
// 替换本地变量占位符 properties.put(key, value);
Set<Entry<Object, Object>> entrySet = properties.entrySet(); }
for (Entry<Object, Object> entry : entrySet) {
String key = entry.getKey().toString(); return properties;
String value = entry.getValue().toString(); }
addProperty(key, value);
} @SuppressWarnings("unchecked")
private Map<String, Object> fetchConfigFromServer() {
// 加载本地测试配置 Map<String, Object> result = null;
String path = System.getProperty("localtest.config.location"); String errorMsg = null;
if (StringUtils.isNotBlank(path)) { for (String apiBaseUrl : apiBaseUrls) {
try (FileReader fileReader = new FileReader(path)) { String url = apiBaseUrl + "/api/fetchAllConfigs";
Properties localProperties = new Properties(); Map<String, Object> params = new HashMap<>();
localProperties.load(fileReader); params.put("appName", app);
if (!localProperties.isEmpty()) { params.put("env", env);
properties.putAll(localProperties); params.put("version", version);
} params.put("ignoreGlobal", ignoreGlobal);
} catch (Exception e) { if (token != null && !token.isEmpty()) {
logger.warn("Failed to load local test config from {}: {}", path, e.getMessage()); params.put("token", token);
} }
} logger.info("fetch configs url:{}", url);
try {
// Close existing listener before creating new one String jsonString = httpPostJson(url, JSON.toJSONString(params));
if (configChangeListener != null) { if (jsonString != null) {
configChangeListener.close(); result = JSON.parseObject(jsonString, Map.class);
} if (result.containsKey("code")) {
// register listener errorMsg = result.get("msg").toString();
configChangeListener = new InternalConfigChangeListener(); logger.error("fetch error:{}", errorMsg);
printConfigs(properties); result = null;
} } else {
break;
private Properties getAllRemoteProperties() { }
if (remoteProperties != null) return remoteProperties; }
Properties properties = new Properties(); } catch (Exception e) {
logger.error("fetch config error from {}: {}", apiBaseUrl, e.getMessage());
Map<String, Object> map = fetchConfigFromServer(); }
if (map == null) { }
throw new RuntimeException("fetch remote config error!");
} if (result == null) {
logger.warn(">>>>>remote config fetch error, load from local Cache");
// 解密密匙 result = LocalCacheUtils.read();
secret = Objects.toString(map.remove("kenaito.config.encrypt-secret"), null); } else {
globalSecret = Objects.toString(map.remove("kenaito.config.global-encrypt-secret"), null); LocalCacheUtils.write(result);
remoteFirst = Boolean.parseBoolean(Objects.toString(map.remove("kenaito.config.remote-config-first"), "false")); }
return result;
Set<String> keys = map.keySet(); }
for (String key : keys) {
Object value = decodeEncryptIfRequire(map.get(key)); private void printConfigs(Properties properties) {
properties.put(key, value); List<String> sortKeys = new ArrayList<>();
} Set<Entry<Object, Object>> entrySet = properties.entrySet();
for (Entry<Object, Object> entry : entrySet) {
return properties; sortKeys.add(entry.getKey().toString());
} }
Collections.sort(sortKeys);
@SuppressWarnings("unchecked") System.out.println("==================final config list start==================");
private Map<String, Object> fetchConfigFromServer() { for (String key : sortKeys) {
Map<String, Object> result = null; String value = hideSensitive(key, properties.getProperty(key));
String errorMsg = null; System.out.printf("%s = %s%n", key, value);
for (String apiBaseUrl : apiBaseUrls) { }
String url = apiBaseUrl + "/api/fetchAllConfigs"; System.out.println("==================final config list end====================");
Map<String, Object> params = new HashMap<>(); }
params.put("appName", app);
params.put("env", env); @SuppressWarnings("unchecked")
params.put("version", version); public synchronized void updateConfig(Map<String, Object> updateConfig) {
params.put("ignoreGlobal", ignoreGlobal); if (!updateConfig.isEmpty()) {
if (token != null && !token.isEmpty()) { Set<String> keySet = updateConfig.keySet();
params.put("token", token); for (String key : keySet) {
} String oldValue = getProperty(key);
logger.info("fetch configs url:{}", url); addProperty(key, updateConfig.get(key).toString());
try { logger.info("Config [{}] Change, oldValue:{}, newValue:{}", key, oldValue, updateConfig.get(key));
String jsonString = httpPostJson(url, JSON.toJSONString(params)); }
if (jsonString != null) {
result = JSON.parseObject(jsonString, Map.class); // Update Spring Environment property source
if (result.containsKey("code")) { if (applicationContext != null) {
errorMsg = result.get("msg").toString(); StandardEnvironment environment = (StandardEnvironment) applicationContext.getEnvironment();
logger.error("fetch error:{}", errorMsg); MutablePropertySources propertySources = environment.getPropertySources();
result = null; synchronized (propertySources) {
} else { Properties properties = new Properties();
break; if (propertySources.contains(MANAGER_PROPERTY_SOURCE)) {
} Object source = propertySources.get(MANAGER_PROPERTY_SOURCE);
} if (source instanceof MapPropertySource) {
} catch (Exception e) { Map<String, Object> map = ((MapPropertySource) source).getSource();
logger.error("fetch config error from {}: {}", apiBaseUrl, e.getMessage()); for (Entry<String, Object> entry : map.entrySet()) {
} properties.put(entry.getKey(), entry.getValue());
} }
} else if (source instanceof PropertiesPropertySource) {
if (result == null) { Map<String, Object> src = ((PropertiesPropertySource) source).getSource();
logger.warn(">>>>>remote config fetch error, load from local Cache"); for (Entry<String, Object> entry : src.entrySet()) {
result = LocalCacheUtils.read(); properties.put(entry.getKey(), entry.getValue().toString());
} else { }
LocalCacheUtils.write(result); }
} }
return result; for (Entry<String, Object> entry : updateConfig.entrySet()) {
} properties.put(entry.getKey(), entry.getValue());
}
private void printConfigs(Properties properties) { propertySources.replace(MANAGER_PROPERTY_SOURCE, new PropertiesPropertySource(MANAGER_PROPERTY_SOURCE, properties));
List<String> sortKeys = new ArrayList<>(); }
Set<Entry<Object, Object>> entrySet = properties.entrySet(); }
for (Entry<Object, Object> entry : entrySet) {
sortKeys.add(entry.getKey().toString()); if (configChangeHanlders == null) {
} configChangeHanlders = new ArrayList<>();
Collections.sort(sortKeys); if (applicationContext != null) {
System.out.println("==================final config list start=================="); Map<String, ConfigChangeHanlder> interfaces = applicationContext.getBeansOfType(ConfigChangeHanlder.class);
for (String key : sortKeys) { if (interfaces != null) {
String value = hideSensitive(key, properties.getProperty(key)); configChangeHanlders.addAll(interfaces.values());
System.out.printf("%s = %s%n", key, value); }
} }
System.out.println("==================final config list end===================="); }
}
for (ConfigChangeHanlder handler : configChangeHanlders) {
@SuppressWarnings("unchecked") try {
public synchronized void updateConfig(Map<String, Object> updateConfig) { handler.onConfigChanged(updateConfig);
if (!updateConfig.isEmpty()) { logger.info("invoke {}.onConfigChanged successed!", handler.getClass().getName());
Set<String> keySet = updateConfig.keySet(); } catch (Exception e) {
for (String key : keySet) { logger.warn("invoke {}.onConfigChanged error, msg:{}", handler.getClass().getName(), e.getMessage());
String oldValue = getProperty(key); }
addProperty(key, decodeEncryptIfRequire(updateConfig.get(key)).toString()); }
logger.info("Config [{}] Change, oldValue:{}, newValue:{}", key, oldValue, updateConfig.get(key));
} // Refresh @Value and @ConfigurationProperties fields
logger.info("[RefreshScope] Calling refreshDynamicConfigFields()...");
// Update Spring Environment property source refreshDynamicConfigFields();
if (applicationContext != null) { logger.info("[RefreshScope] refreshDynamicConfigFields() completed");
StandardEnvironment environment = (StandardEnvironment) applicationContext.getEnvironment(); }
MutablePropertySources propertySources = environment.getPropertySources(); }
synchronized (propertySources) {
Properties properties = new Properties(); private void refreshDynamicConfigFields() {
if (propertySources.contains(MANAGER_PROPERTY_SOURCE)) { if (applicationContext != null) {
Object source = propertySources.get(MANAGER_PROPERTY_SOURCE); try {
if (source instanceof MapPropertySource) { logger.info("[RefreshScope] Starting dynamic config field refresh...");
Map<String, Object> map = ((MapPropertySource) source).getSource(); StandardEnvironment env = (StandardEnvironment) applicationContext.getEnvironment();
for (Entry<String, Object> entry : map.entrySet()) { // 直接创建 RefreshScopeRegistry 并注册为 BeanPostProcessor
properties.put(entry.getKey(), entry.getValue()); RefreshScopeRegistry registry = new RefreshScopeRegistry(env);
}
} else if (source instanceof PropertiesPropertySource) { // 遍历所有已注册的 Bean,记录 @Value 和 @ConfigurationProperties 字段
Map<String, Object> src = ((PropertiesPropertySource) source).getSource(); String[] beanNames = applicationContext.getBeanDefinitionNames();
for (Entry<String, Object> entry : src.entrySet()) { logger.info("[RefreshScope] Found {} beans in ApplicationContext", beanNames.length);
properties.put(entry.getKey(), entry.getValue().toString());
} int valueFieldCount = 0;
} int configPropsCount = 0;
} for (String beanName : beanNames) {
for (Entry<String, Object> entry : updateConfig.entrySet()) { Object bean = applicationContext.getBean(beanName);
properties.put(entry.getKey(), entry.getValue()); registry.postProcessAfterInitialization(bean, beanName);
} }
propertySources.replace(MANAGER_PROPERTY_SOURCE, new PropertiesPropertySource(MANAGER_PROPERTY_SOURCE, properties));
} valueFieldCount = registry.getValueFieldCount();
} configPropsCount = registry.getConfigPropsCount();
logger.info("[RefreshScope] Recorded {} @Value fields and {} @ConfigurationProperties beans", valueFieldCount, configPropsCount);
if (configChangeHanlders == null) {
configChangeHanlders = new ArrayList<>(); // 刷新
if (applicationContext != null) { registry.refresh();
Map<String, ConfigChangeHanlder> interfaces = applicationContext.getBeansOfType(ConfigChangeHanlder.class); logger.info("[RefreshScope] RefreshScopeRegistry.refresh() completed, refreshed {} @Value fields and {} @ConfigurationProperties beans", registry.getValueFieldCount(), registry.getConfigPropsCount());
if (interfaces != null) { } catch (Exception e) {
configChangeHanlders.addAll(interfaces.values()); logger.warn("[RefreshScope] RefreshScopeRegistry.refresh() error, msg:{}", e.getMessage(), e);
} }
} } else {
} logger.warn("[RefreshScope] ApplicationContext is null, cannot refresh dynamic config fields");
}
for (ConfigChangeHanlder handler : configChangeHanlders) { }
try {
handler.onConfigChanged(updateConfig); public String buildTokenParameter(String url) {
logger.info("invoke {}.onConfigChanged successed!", handler.getClass().getName()); if (token == null || token.isEmpty()) {
} catch (Exception e) { return url;
logger.warn("invoke {}.onConfigChanged error, msg:{}", handler.getClass().getName(), e.getMessage()); }
} return url + (url.contains("?") ? "&" : "?") + "token=" + token;
} }
// Refresh @Value and @ConfigurationProperties fields public void close() {
logger.info("[RefreshScope] Calling refreshDynamicConfigFields()..."); if (configChangeListener != null) {
refreshDynamicConfigFields(); configChangeListener.close();
logger.info("[RefreshScope] refreshDynamicConfigFields() completed"); }
} }
}
private String hideSensitive(String key, String origin) {
private void refreshDynamicConfigFields() { if (StringUtils.isAnyBlank(key, origin)) return "";
if (applicationContext != null) { boolean isSensitive = false;
try { for (String k : sensitiveKeys) {
logger.info("[RefreshScope] Starting dynamic config field refresh..."); if (key.toLowerCase().contains(k)) {
StandardEnvironment env = (StandardEnvironment) applicationContext.getEnvironment(); isSensitive = true;
// 直接创建 RefreshScopeRegistry 并注册为 BeanPostProcessor break;
RefreshScopeRegistry registry = new RefreshScopeRegistry(env); }
}
// 遍历所有已注册的 Bean,记录 @Value 和 @ConfigurationProperties 字段 int length = origin.length();
String[] beanNames = applicationContext.getBeanDefinitionNames(); if (isSensitive && length > 1) return origin.substring(0, length / 2).concat("****");
logger.info("[RefreshScope] Found {} beans in ApplicationContext", beanNames.length); return origin;
}
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");
}
}
public boolean pingCcServer(String pingUrl, int retry) {
boolean result = false;
try {
result = httpGet(pingUrl) != null;
} catch (Exception e) {
// ignore
}
if (retry == 0) return false;
if (!result) {
try { Thread.sleep(1500); } catch (Exception e) { }
return pingCcServer(pingUrl, --retry);
}
return result;
}
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)) {
Validate.notBlank(secret, "config[kenaito.config.encrypt-secret] is required");
data = data.toString().replace(CRYPT_PREFIX, "");
String decryptString;
try {
decryptString = decryptWithAES(secret, data.toString());
} catch (Exception e) {
if (globalSecret != null && !globalSecret.isEmpty()) {
decryptString = decryptWithAES(globalSecret, data.toString());
} else {
throw e;
}
}
return decryptString;
}
return data;
}
private String hideSensitive(String key, String origin) {
if (StringUtils.isAnyBlank(key, origin)) 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;
}
} }
@@ -1,24 +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;
}
/**
* @return the keys
*/
public Set<String> getKeys() {
return keys;
}
}
-7
View File
@@ -14,7 +14,6 @@ type Config struct {
Server ServerConfig Server ServerConfig
Database DatabaseConfig Database DatabaseConfig
JWT JWTConfig JWT JWTConfig
Crypto CryptoConfig
} }
type ServerConfig struct { type ServerConfig struct {
@@ -35,10 +34,6 @@ type JWTConfig struct {
ExpireHours int ExpireHours int
} }
type CryptoConfig struct {
SecretKey string
}
var Cfg *Config var Cfg *Config
func Load(path string) error { func Load(path string) error {
@@ -77,8 +72,6 @@ func Load(path string) error {
Cfg.JWT.Secret = baseProps["jwt.secret"] Cfg.JWT.Secret = baseProps["jwt.secret"]
Cfg.JWT.ExpireHours = intProp(baseProps, "jwt.expireHours", 24) Cfg.JWT.ExpireHours = intProp(baseProps, "jwt.expireHours", 24)
Cfg.Crypto.SecretKey = baseProps["crypto.secretKey"]
if Cfg.Database.Host == "" { if Cfg.Database.Host == "" {
return fmt.Errorf("missing required config: database.host") return fmt.Errorf("missing required config: database.host")
} }