模仿nacos实现自己的配置中心
0. 配置中心简单交互
- 编写自己的sdk:拉取配置、服务器端更新后客户端能感知到并且更新到本地
- 和Springboot 做整合:(依赖Springcloud)
(1). Springcloud 预留了做配置中心的接口,相当于是注入自己的PropertySourceLocator, Springcloud 环境启动过程中会读取bootstrap.properties, 然后进行Springcloud 环境初始化(包括加载PropertySourceLocator)
(2). 配置更新后通知Spring的environment和@Value 注入进去的bean。 依赖于Springcloud 提供的@RefreshScope 注解以及发布RefreshEvent 事件。
@RefreshScope 相当于重写Spring Bean 的作用域,在org.springframework.cloud.context.scope.refresh.RefreshScope 获取对象(逻辑交给父类,实际类似单例缓存到内部);
接收到RefreshEvent 事件之后org.springframework.cloud.endpoint.event.RefreshEventListener 进行处理,最后调用到RefreshScope清除缓存,下次调用容器的getBean就会重新建对象以及依赖注入。
1. pom
<?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>com.bj58</groupId>
<artifactId>spring-cloud-starter-custom-config</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 编写自己的sdk
一般配置中心分为两端。server端和client端。server 端用于配置的CRUD以及对外暴露接口,供SDK拉取配置以及接收变更后的配置。这里简单模拟本地map拉取以及更新。
package com.demo.test.sdk;
import com.demo.test.constant.ConfigKey;
import org.apache.commons.lang3.RandomStringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 配置拉取客户端
* <p>
* 模糊从文件获取。 真实的可以从server 服务端拉取
*/
public class ConfigLoadComponent {
public static Map<String, String> getAllConfig(String policyName) {
// 本地构造数据
Map<String, String> hashMap = new HashMap<>();
hashMap.put(policyName, RandomStringUtils.randomAlphabetic(10));
hashMap.put(ConfigKey.KEY1, RandomStringUtils.randomAlphabetic(10));
Map<String, String> result = CacheData.getInstance().putIfAbsent(policyName, hashMap);
return result;
}
}
package com.demo.test.sdk;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 关联Spring环境和非Spring 环境的对象
*/
@Slf4j
public class CacheData {
/**
* 单例维护
*/
public static CacheData INSTANCE = new CacheData();
public static CacheData getInstance() {
return INSTANCE;
}
/**
* 维护缓存的数据
*/
private final ConcurrentHashMap<String, Map<String, String>> CHCHE = new ConcurrentHashMap<>();
/**
* 维护监听器
*/
private final List<ConfigRefreshListener> listeners = new ArrayList<>();
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
public CacheData() {
log.info("CacheData created");
// 开启定时任务更新缓存
scheduledExecutorService.scheduleWithFixedDelay(() -> {
log.info("refresh task start");
// 模拟定时任务更新key
CHCHE.forEach((k, v) -> {
log.info("refresh key {}, value: {}", k, v);
// 更新缓存
v.forEach((k1, v1) -> {
v.put(k1, RandomStringUtils.randomAlphabetic(10));
});
log.info("refresh key {}, value: {}", k, v);
// 发通知
for (ConfigRefreshListener listener : listeners) {
listener.onRefreshed(k);
}
log.info("refresh key onRefreshed {}", listeners.size());
});
}, 0, 10, TimeUnit.SECONDS);
}
public Map<String, String> putIfAbsent(String key, Map<String, String> dafaultVal) {
return CHCHE.putIfAbsent(key, dafaultVal);
}
public void addListener(List<ConfigRefreshListener> listeners) {
this.listeners.addAll(listeners);
}
}
package com.demo.test.sdk;
public interface ConfigRefreshListener {
void onRefreshed(String cacheKey);
}
3. 实现服务启动拉取配置
1. 重要类
ConfigProperties (配置类)
package com.demo.test;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(ConfigProperties.PREFIX)
public class ConfigProperties {
public static final String PREFIX = "spring.cloud.config";
private String policyName;
public String getPolicyName() {
return policyName;
}
public void setPolicyName(String policyName) {
this.policyName = policyName;
}
}
CustomConfigPropertySource、CustomConfigPropertySourceBuilder、CustomConfigPropertySourceLocator
package com.demo.test.client;
import org.springframework.core.env.MapPropertySource;
import java.util.Date;
import java.util.Map;
public class CustomConfigPropertySource extends MapPropertySource {
private final String dataId;
private final Date timestamp;
private final boolean isRefreshable;
CustomConfigPropertySource(String dataId, Map<String, Object> source, Date timestamp, boolean isRefreshable) {
super(dataId, source);
this.dataId = dataId;
this.timestamp = timestamp;
this.isRefreshable = isRefreshable;
}
}
package com.demo.test.client;
import com.demo.test.ConfigProperties;
import com.demo.test.sdk.ConfigLoadComponent;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class CustomConfigPropertySourceBuilder {
private ConfigProperties configProperties;
public CustomConfigPropertySourceBuilder(ConfigProperties configProperties) {
this.configProperties = configProperties;
}
CustomConfigPropertySource build(String dataId, boolean isRefreshable) {
// 调用自己的SDK 拉取配置
String policyName = configProperties.getPolicyName();
Map<String, Object> result = new HashMap<>();
Map<String, String> allConfig = ConfigLoadComponent.getAllConfig(policyName);
if (allConfig != null && allConfig.size() > 0) {
allConfig.forEach((k, v) -> {
result.put(k, v);
});
}
log.info("build property, result: {}", result);
CustomConfigPropertySource algConfigPropertySource = new CustomConfigPropertySource(dataId, result, new Date(), isRefreshable);
return algConfigPropertySource;
}
}
package com.demo.test.client;
import com.demo.test.ConfigProperties;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
public class CustomConfigPropertySourceLocator implements PropertySourceLocator {
private static final String ALG_CONFIG_PROPERTY_SOURCE_NAME = "CUSTOM_CONFIG";
private static final boolean isRefreshable = true;
private CustomConfigPropertySourceBuilder customConfigPropertySourceBuilder;
private ConfigProperties configProperties;
public CustomConfigPropertySourceLocator(ConfigProperties algConfigProperties) {
this.configProperties = algConfigProperties;
this.customConfigPropertySourceBuilder = new CustomConfigPropertySourceBuilder(algConfigProperties);
}
@Override
public PropertySource<?> locate(Environment environment) {
CompositePropertySource composite = new CompositePropertySource(ALG_CONFIG_PROPERTY_SOURCE_NAME);
String dataId = configProperties.getPolicyName();
CustomConfigPropertySource ps = customConfigPropertySourceBuilder.build(dataId, isRefreshable);
composite.addFirstPropertySource(ps);
// 遍历激活的环境进行获取
// for (String profile : environment.getActiveProfiles()) {
// String dataId = policyName + "-" + profile;
// AlgConfigPropertySource ps = algConfigPropertySourceBuilder.build(dataId, isRefreshable);
// composite.addFirstPropertySource(ps);
// }
return composite;
}
}
2. 自动配置类
package com.demo.test;
import com.demo.test.client.CustomConfigPropertySourceLocator;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
//@Configuration
//@ConditionalOnProperty(name = "spring.cloud.alg.config.enabled", matchIfMissing = true)
public class CustomConfigAutoConfiguration {
@Bean
public ConfigProperties configProperties(ApplicationContext context) {
if (context.getParent() != null
&& BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
context.getParent(), ConfigProperties.class).length > 0) {
return BeanFactoryUtils.beanOfTypeIncludingAncestors(context.getParent(),
ConfigProperties.class);
}
ConfigProperties nacosConfigProperties = new ConfigProperties();
return nacosConfigProperties;
}
@Bean
public CustomConfigPropertySourceLocator customConfigPropertySourceLocator(ConfigProperties configProperties) {
return new CustomConfigPropertySourceLocator(configProperties);
}
}
3. bootstrap.properties 配置
spring.cloud.config.policyName=qlq_test02
server.port=8090
4.resources/META-INF/spring.factories 文件
增加如下 springcloud 自动配置
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.demo.test.CustomConfigAutoConfiguration
5. 测试
到这里可以实现服务启动后自动拉取配置,且注入到Spring的Environment 对象中。 只需要自己完成CustomConfigPropertySourceBuilder 类中调用自己的SDK从服务器端拉取配置(可以走RPC或者HTTP)
4. 自动刷新
1. 重要类
- 自己的业务listener (监听到数据改变后发布事件)
package com.demo.test.refresh;
import com.demo.test.sdk.ConfigRefreshListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.cloud.endpoint.event.RefreshEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
/**
* 监听key改变,然后发送事件
*/
@Slf4j
public class CustomConfigRefreshListerer implements ConfigRefreshListener, ApplicationContextAware {
private ApplicationContext context;
@Override
public void onRefreshed(String cacheKey) {
log.info("refresh key: {}", cacheKey);
context.publishEvent(new RefreshEvent(this, null, "Refresh config " + cacheKey));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
}
- Refresher: 将listener维护到CacheData
package com.demo.test.refresh;
import com.demo.test.sdk.CacheData;
import com.demo.test.sdk.ConfigRefreshListener;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import java.util.List;
public class CustomConfigRefresher implements ApplicationListener<ApplicationReadyEvent> {
private List<ConfigRefreshListener> listeners;
public CustomConfigRefresher(List<ConfigRefreshListener> listeners) {
this.listeners = listeners;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
CacheData.getInstance().addListener(listeners);
}
}
2. ConfigRefresherAutoConfiguration 自动配置类
package com.demo.test;
import com.demo.test.refresh.CustomConfigRefreshListerer;
import com.demo.test.refresh.CustomConfigRefresher;
import com.demo.test.sdk.ConfigRefreshListener;
import org.springframework.context.annotation.Bean;
import java.util.List;
public class ConfigRefresherAutoConfiguration {
@Bean
public CustomConfigRefreshListerer customConfigRefreshListerer() {
return new CustomConfigRefreshListerer();
}
@Bean
public CustomConfigRefresher customConfigRefresher (List<ConfigRefreshListener> listeners) {
return new CustomConfigRefresher(listeners);
}
}
3. resources/META-INF/spring.factories 文件
增加如下Springboot 自动配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.demo.test.ConfigRefresherAutoConfiguration
5. 测试类
package com.demo.test.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping
@RefreshScope
public class TestController {
@Autowired
private Environment environment;
@Value("${key1:''}")
private String test2;
@GetMapping("/test")
public String test() {
return environment.getProperty("key1");
}
@GetMapping("/test2")
public String test2() {
return test2;
}
@GetMapping("/test3")
public String test3() {
return this.toString();
}
}
【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2022-03-28 消息重发、重试消费、死信队列
2019-03-28 EmbeddedSolrServer的使用与solor6.3.0的使用
2018-03-28 linux系统引导流程
2018-03-28 CentOS7修改默认运行级别
2018-03-28 关于Linux下s、t、i、a权限