feat: 新增 Spring Boot 3 客户端 SDK 及 demo

- 新增 kenaito-conf-springboot3-client,支持配置拉取、增量同步、@Value/@ConfigurationProperties 动态刷新和本地缓存

- 新增 kenaito-conf-springboot3-client-demo(端口 8084)及 Dockerfile.springboot3

- 完善中英文 README、Docker 部署文档,修正 Spring Boot 2 客户端配置键名与 demo 端口说明

- 修复增量同步将服务端 code=0 成功响应误判为错误的问题
This commit is contained in:
2026-08-22 21:02:15 +08:00
parent 5f67274e3f
commit 182765e4a0
28 changed files with 2566 additions and 57 deletions
@@ -0,0 +1,5 @@
/target/
.DS_Store
/.settings/
.project
.classpath
@@ -0,0 +1,49 @@
<?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 config center (Spring Boot 3)</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.16</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<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 Spring Boot 3 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>
@@ -0,0 +1,44 @@
package cn.odboy.demo;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import cn.odboy.confcenter.ConfigChangeHanlder;
/**
* Example {@link ConfigChangeHanlder} that logs changes and keeps the 20 most
* recent ones for the {@code /changes} endpoint.
*
* @author odboy
* @date 2026-10-21
*/
@Component
public class ConfigChangeLogger implements ConfigChangeHanlder {
private static final Logger logger = LoggerFactory.getLogger(ConfigChangeLogger.class);
private static final int MAX_HISTORY = 20;
private final Deque<Map<String, Object>> history = new ArrayDeque<>();
@Override
public synchronized void onConfigChanged(Map<String, Object> changedConfigs) {
logger.info("[Demo] config changed: {}", changedConfigs);
history.addFirst(new LinkedHashMap<>(changedConfigs));
while (history.size() > MAX_HISTORY) {
history.removeLast();
}
}
public synchronized List<Map<String, Object>> getHistory() {
return new ArrayList<>(history);
}
}
@@ -0,0 +1,96 @@
package cn.odboy.demo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Demo endpoints showing {@code @Value} and {@code @ConfigurationProperties}
* values refresh when configs change in the config center.
*
* @author odboy
* @date 2026-10-21
*/
@RestController
public class ConfigController {
@Value("${database.host:unknown}")
private String host;
@Value("${database.port:unknown}")
private String port;
@Value("${database.name:unknown}")
private String name;
@Value("${database.user:unknown}")
private String user;
@Value("${database.password:unknown}")
private String password;
@Value("${app.name:unknown}")
private String appName;
private final DatabaseProperties databaseProperties;
private final ConfigChangeLogger configChangeLogger;
private final Environment environment;
@Autowired
public ConfigController(DatabaseProperties databaseProperties,
ConfigChangeLogger configChangeLogger,
Environment environment) {
this.databaseProperties = databaseProperties;
this.configChangeLogger = configChangeLogger;
this.environment = environment;
}
@GetMapping("/")
public Map<String, Object> index() {
return Map.of(
"app", appName,
"profiles", String.join(",", environment.getActiveProfiles()),
"configCenter", environment.getProperty("kenaito.config.base-url", ""),
"database.host", host);
}
@GetMapping("/config")
public Map<String, Object> config() {
Map<String, Object> result = new HashMap<>();
result.put("host", host);
result.put("port", port);
result.put("name", name);
result.put("user", user);
result.put("password", password);
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("/changes")
public List<Map<String, Object>> changes() {
return configChangeLogger.getHistory();
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "UP");
}
}
@@ -0,0 +1,65 @@
package cn.odboy.demo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Database configuration bound with {@code @ConfigurationProperties}.
*
* @author odboy
* @date 2026-10-21
*/
@Component
@ConfigurationProperties(prefix = "database")
public class DatabaseProperties {
private String host;
private Integer port;
private String name;
private String user;
private String password;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return port;
}
public void setPort(Integer 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;
}
}
@@ -0,0 +1,18 @@
package cn.odboy.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Kenaito Config Center Spring Boot 3 demo application.
*
* @author odboy
* @date 2026-10-21
*/
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@@ -0,0 +1,23 @@
# Server
server.port=8084
# Application
spring.application.name=kenaito-conf-springboot3-client-demo
app.name=kenaito-conf-springboot3-client-demo
app.version=1.0.0
# Kenaito Config Center
kenaito.config.base-url=${KENAITO_BASE_URL:http://localhost:8080}
kenaito.config.app-name=${KENAITO_APP_NAME:kenaito-springboot-demo}
kenaito.config.profile=${KENAITO_ENV:daily}
kenaito.config.version=${KENAITO_VERSION:latest}
kenaito.config.token=${KENAITO_TOKEN:}
kenaito.config.sync-interval-seconds=${KENAITO_SYNC_INTERVAL:10}
kenaito.config.sensitive-keys=password,secret,key,token
# Local configs (overridden by remote configs when they exist)
database.host=192.168.1.1
database.port=8888
database.name=xxxx
database.user=xxxx
database.password=xxxxxxxx