feat(clients): 各客户端支持环境变量配置,Spring Boot 2 客户端优化选择性刷新

- Dockerfile.springboot2: 更换基础镜像为阿里云私有镜像
- .NET/SpringBoot2 demo: 配置项改为从环境变量读取,支持 Docker 传参
- Spring Boot 2 客户端: @Value/@ConfigurationProperties 刷新优化为只刷新受变更 key 影响的字段
- README_DOCKERFILE.md: 标注 .NET 客户端 Docker 测试通过
This commit is contained in:
2026-07-02 21:16:53 +08:00
parent d55ef1dfa6
commit 75443cd39e
7 changed files with 53 additions and 40 deletions
+3 -2
View File
@@ -1,5 +1,5 @@
# Build stage # Build stage
FROM maven:3.9-eclipse-temurin-8 AS build FROM registry.cn-shanghai.aliyuncs.com/odboy/kenaito-cicd:runtime-maven3-jdk21 AS build
WORKDIR /src WORKDIR /src
@@ -9,12 +9,13 @@ WORKDIR /src/kenaito-conf-springboot2-client
RUN mvn clean install -DskipTests RUN mvn clean install -DskipTests
# Copy demo project and build # Copy demo project and build
WORKDIR /src
COPY kenaito-conf-springboot2-client-demo/ ./kenaito-conf-springboot2-client-demo/ COPY kenaito-conf-springboot2-client-demo/ ./kenaito-conf-springboot2-client-demo/
WORKDIR /src/kenaito-conf-springboot2-client-demo WORKDIR /src/kenaito-conf-springboot2-client-demo
RUN mvn clean package -DskipTests RUN mvn clean package -DskipTests
# Runtime stage # Runtime stage
FROM eclipse-temurin:8-jre FROM registry.cn-shanghai.aliyuncs.com/odboy/kenaito-cicd:runtime-jdk8
WORKDIR /app WORKDIR /app
COPY --from=build /src/kenaito-conf-springboot2-client-demo/target/*.jar app.jar COPY --from=build /src/kenaito-conf-springboot2-client-demo/target/*.jar app.jar
+1 -1
View File
@@ -62,7 +62,7 @@ curl http://localhost:8082/health
docker stop python-demo docker stop python-demo
``` ```
## .NET 客户端 ## .NET 客户端(测试通过)
```bash ```bash
docker build -f Dockerfile.dotnet -t kenaito-conf-dotnet-demo . docker build -f Dockerfile.dotnet -t kenaito-conf-dotnet-demo .
+7 -6
View File
@@ -2,12 +2,13 @@ using Kenaito.Conf.Client;
var config = new Config var config = new Config
{ {
BaseUrl = "http://localhost:8080", BaseUrl = Environment.GetEnvironmentVariable("KENAITO_BASE_URL") ?? "http://localhost:8080",
AppName = "kenaito-springboot-demo", AppName = Environment.GetEnvironmentVariable("KENAITO_APP_NAME") ?? "kenaito-springboot-demo",
Env = "daily", Env = Environment.GetEnvironmentVariable("KENAITO_ENV") ?? "daily",
Version = "latest", Version = Environment.GetEnvironmentVariable("KENAITO_VERSION") ?? "latest",
SyncInterval = 10, SyncInterval = int.TryParse(Environment.GetEnvironmentVariable("KENAITO_SYNC_INTERVAL"), out var si) ? si : 10,
CacheDir = "." CacheDir = Environment.GetEnvironmentVariable("KENAITO_CACHE_DIR") ?? ".",
Token = Environment.GetEnvironmentVariable("KENAITO_TOKEN")
}; };
var client = new Client(config); var client = new Client(config);
@@ -6,12 +6,12 @@ app.name=kenaito-conf-springboot2-client-demo
app.version=1.0.0 app.version=1.0.0
# Kenaito Config Center # Kenaito Config Center
kenaito.config.baseUrl=http://localhost:8080 kenaito.config.baseUrl=${KENAITO_BASE_URL:http://localhost:8080}
kenaito.config.appName=kenaito-springboot-demo kenaito.config.appName=${KENAITO_APP_NAME:kenaito-springboot-demo}
kenaito.config.profile=daily kenaito.config.profile=${KENAITO_ENV:daily}
kenaito.config.version=latest kenaito.config.version=${KENAITO_VERSION:latest}
kenaito.config.token= kenaito.config.token=${KENAITO_TOKEN:}
kenaito.config.sync-interval-seconds=10 kenaito.config.sync-interval-seconds=${KENAITO_SYNC_INTERVAL: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
@@ -333,6 +333,8 @@ public class ConfigcenterContext {
result = LocalCacheUtils.read(); result = LocalCacheUtils.read();
} else { } else {
LocalCacheUtils.write(result); LocalCacheUtils.write(result);
int configCount = result.size();
logger.info("[ConfigClient] fetched {} configs", configCount);
} }
return result; return result;
} }
@@ -410,7 +412,7 @@ public class ConfigcenterContext {
// Refresh @Value and @ConfigurationProperties fields // Refresh @Value and @ConfigurationProperties fields
logger.info("[RefreshScope] Calling refreshDynamicConfigFields()..."); logger.info("[RefreshScope] Calling refreshDynamicConfigFields()...");
refreshDynamicConfigFields(); refreshDynamicConfigFields(updateConfig.keySet());
logger.info("[RefreshScope] refreshDynamicConfigFields() completed"); logger.info("[RefreshScope] refreshDynamicConfigFields() completed");
} }
} }
@@ -448,13 +450,13 @@ public class ConfigcenterContext {
} }
} }
// Refresh @Value and @ConfigurationProperties fields // Refresh @Value and @ConfigurationProperties fields
refreshDynamicConfigFields(); refreshDynamicConfigFields(new HashSet<>(keys));
} }
private void refreshDynamicConfigFields() { private void refreshDynamicConfigFields(Set<String> changedKeys) {
if (applicationContext != null) { if (applicationContext != null) {
try { try {
logger.info("[RefreshScope] Starting dynamic config field refresh..."); logger.info("[RefreshScope] Starting dynamic config field refresh for keys: {}", String.join(", ", changedKeys));
StandardEnvironment env = (StandardEnvironment) applicationContext.getEnvironment(); StandardEnvironment env = (StandardEnvironment) applicationContext.getEnvironment();
// 直接创建 RefreshScopeRegistry 并注册为 BeanPostProcessor // 直接创建 RefreshScopeRegistry 并注册为 BeanPostProcessor
RefreshScopeRegistry registry = new RefreshScopeRegistry(env); RefreshScopeRegistry registry = new RefreshScopeRegistry(env);
@@ -474,8 +476,8 @@ public class ConfigcenterContext {
configPropsCount = registry.getConfigPropsCount(); configPropsCount = registry.getConfigPropsCount();
logger.info("[RefreshScope] Recorded {} @Value fields and {} @ConfigurationProperties beans", valueFieldCount, configPropsCount); logger.info("[RefreshScope] Recorded {} @Value fields and {} @ConfigurationProperties beans", valueFieldCount, configPropsCount);
// 刷新 // 刷新(只刷新受影响的字段)
registry.refresh(); registry.refresh(changedKeys);
logger.info("[RefreshScope] RefreshScopeRegistry.refresh() completed, refreshed {} @Value fields and {} @ConfigurationProperties beans", registry.getValueFieldCount(), registry.getConfigPropsCount()); logger.info("[RefreshScope] RefreshScopeRegistry.refresh() completed, refreshed {} @Value fields and {} @ConfigurationProperties beans", registry.getValueFieldCount(), registry.getConfigPropsCount());
} catch (Exception e) { } catch (Exception e) {
logger.warn("[RefreshScope] RefreshScopeRegistry.refresh() error, msg:{}", e.getMessage(), e); logger.warn("[RefreshScope] RefreshScopeRegistry.refresh() error, msg:{}", e.getMessage(), e);
@@ -72,7 +72,10 @@ public class InternalConfigChangeListener {
logger.info("[ConfigClient] {} configs deleted: {}", deletedKeys.length, String.join(", ", deletedKeys)); logger.info("[ConfigClient] {} configs deleted: {}", deletedKeys.length, String.join(", ", deletedKeys));
} }
} }
if (!data.isEmpty()) context.updateConfig(data); if (!data.isEmpty()) {
context.updateConfig(data);
logger.info("[ConfigClient] {} configs changed: {}", data.size(), String.join(", ", data.keySet()));
}
} }
} }
} catch (Exception e) { } catch (Exception e) {
@@ -3,6 +3,7 @@ package cn.odboy.confcenter;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -60,27 +61,29 @@ public class RefreshScopeRegistry implements BeanPostProcessor {
} }
/** /**
* Refresh all @Value fields and @ConfigurationProperties beans * Refresh only @Value fields and @ConfigurationProperties beans that are affected by changed keys
*/ */
public void refresh() { public void refresh(Set<String> changedKeys) {
refreshValueFields(); refreshValueFields(changedKeys);
refreshConfigPropsBeans(); refreshConfigPropsBeans(changedKeys);
} }
private void refreshValueFields() { private void refreshValueFields(Set<String> changedKeys) {
for (ValueFieldHolder holder : valueFields) { for (ValueFieldHolder holder : valueFields) {
try { if (changedKeys.contains(holder.key)) {
holder.refresh(environment); try {
} catch (Exception e) { holder.refresh(environment);
// ignore refresh errors } catch (Exception e) {
// ignore refresh errors
}
} }
} }
} }
private void refreshConfigPropsBeans() { private void refreshConfigPropsBeans(Set<String> changedKeys) {
for (ConfigPropsHolder holder : configPropsBeans) { for (ConfigPropsHolder holder : configPropsBeans) {
try { try {
holder.refresh(environment); holder.refresh(environment, changedKeys);
} catch (Exception e) { } catch (Exception e) {
// ignore refresh errors // ignore refresh errors
} }
@@ -149,24 +152,27 @@ public class RefreshScopeRegistry implements BeanPostProcessor {
this.bean = bean; this.bean = bean;
} }
void refresh(Environment env) { void refresh(Environment env, Set<String> changedKeys) {
Class<?> clazz = bean.getClass(); Class<?> clazz = bean.getClass();
ConfigurationProperties anno = clazz.getAnnotation(ConfigurationProperties.class); ConfigurationProperties anno = clazz.getAnnotation(ConfigurationProperties.class);
if (anno != null) { if (anno != null) {
String prefix = anno.prefix(); String prefix = anno.prefix();
bindProperties(bean, prefix, env); bindProperties(bean, prefix, env, changedKeys);
} }
} }
private void bindProperties(Object bean, String prefix, Environment env) { private void bindProperties(Object bean, String prefix, Environment env, Set<String> changedKeys) {
try { try {
Class<?> clazz = bean.getClass(); Class<?> clazz = bean.getClass();
for (Field field : getAllDeclaredFields(clazz)) { for (Field field : getAllDeclaredFields(clazz)) {
String propertyName = prefix + "." + camelToKebab(field.getName()); String propertyName = prefix + "." + camelToKebab(field.getName());
String value = env.getProperty(propertyName); // Only refresh if this property key is in changedKeys
if (value != null) { if (changedKeys.contains(propertyName)) {
field.setAccessible(true); String value = env.getProperty(propertyName);
field.set(bean, convertType(field.getType(), value)); if (value != null) {
field.setAccessible(true);
field.set(bean, convertType(field.getType(), value));
}
} }
} }
} catch (Exception e) { } catch (Exception e) {