文章目录
- Core
- 什么是配置中心?以及如何实现一个配置中心?
- SpringBoot如何实现配置的管控?
- SpringCloud项目是如何对bootstrap配置文件进行加载的?
- Nacos是如何实现配置文件的读取加载的?
- 开发配置中心前必须了解的前置知识
- 配置中心Server和Client端代码的编写
- 配置中心Core核心功能代码的编写
- 配置中心源码优化---本地缓存与读写锁
网关项目源码
RPC项目源码
配置中心项目源码
Core
Core模块是我们项目最核心最重要的模块,当别人需要使用我们的配置中心的时候,只需要引入Core模块,在项目启动的时候就会自动连接我们的配置中心获取配置,并刷新本地的配置。
接下来我们来看看Core模块是如何实现的。
这里,按照我们的前置知识可以知道,我们自上到下,需要完成如下几件事情,我们在复习一下:
- bootstrap配置的获取
- 配置中心的连接与配置的获取
- Locator的实现,加载配置中心的配置
所以我写了一个启动类,也是按照完成这三个事情的顺序,对Bean进行加载。
(proxyBeanMethods = false)
(name = "spring.cloud.blossom.config.enabled", matchIfMissing = true)
public class BlossomConfigBootstrapConfiguration {/*** 项目对配置中心配置* @return*/ public BlossomConfigProperties blossomConfigProperties(){return new BlossomConfigProperties();}/*** 配置中心管理器* @param blossomConfigProperties* @return*/ public BlossomConfigManager blossomConfigManager(BlossomConfigProperties blossomConfigProperties) {return new BlossomConfigManager(blossomConfigProperties);} public BlossomConfigChangePublisher blossomConfigChangePublisher(ApplicationEventPublisher applicationEventPublisher){return new BlossomConfigChangePublisher(applicationEventPublisher);}/*** 配置中心配置加载器* @param blossomConfigManager* @return*/ public BlossomPropertySourceLocator blossomPropertySourceLocator(BlossomConfigManager blossomConfigManager) {return new BlossomPropertySourceLocator(blossomConfigManager);} public BlossomConfigChangeEventSubscriber blossomConfigChangeEventSubscriber(BlossomConfigManager manager,BlossomConfigChangePublisher publisher){return new BlossomConfigChangeEventSubscriber(manager.getConfigService(),publisher);}}
代码实现起来其实非常非常简单。
一切的难点都是在于代码的编写顺序。
这里我们跳过对boostrap配置文件的获取的代码,在前面我们已经提到过了,如果你是SpringCloud项目,直接引入spring-cloud-starter-boostrap依赖就可以直接帮助你完成对bootstrap配置文件的解析,如果你非要手写,那么你就按照前面的方式,编写一个事件监听器来读取你的配置文件。这里我追求效率就不手写了。
那么我们的配置文件就很快的完成了加载。
然后很简单的,我们依赖Client模块提供的配置中心的创建工程来创建Core模块的配置中心。
public class BlossomConfigFactory {/*** 用于创建ConfigService配置中心* @param properties 配置中心的创建需要用到配置文件* @return* @throws BlossomException*/public static ConfigService createConfigService(Properties properties) throws BlossomException {try {Class<?> configServiceClass = Class.forName("blossom.project.config.client.BlossomConfigService");Constructor constructor = configServiceClass.getConstructor(Properties.class);ConfigService configService = (ConfigService) constructor.newInstance(properties);return configService;} catch (Throwable e) {throw new BlossomException(BlossomException.REFLECT_CREATE_ERROR,e.getMessage(), e);}}}
到此为止,我们其实就完成了配置的加载和配置中心的创建。
那么接下来要做的就是初始化配置中心的配置,拉取配置中心的配置。
并加载到本地,作为PropertySource返回。
也是一样,我们重写Locator方法。
package blossom.project.config.core;import blossom.project.config.client.ConfigService;
import blossom.project.config.common.constants.BlossomConstants;
import blossom.project.config.common.enums.ConfigType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;import static blossom.project.config.common.constants.BlossomConstants.DOT;
import static blossom.project.config.common.constants.BlossomConstants.SEPARATOR;/*** @author: ZhangBlossom* @date: 2023/12/28 17:35* @contact: QQ:4602197553* @contact: WX:qczjhczs0114* @blog: https://blog.csdn.net/Zhangsama1* @github: https://github.com/ZhangBlossom* BlossomPropertySourceLocator类* 在编写这个类之前应该先将ConfigService实现类编写完毕* 然后在这个类里面得到ConfigService之后* 调用里面的方法获取到配置中心的配置之后* 将配置加载到本地 同时考虑编写一套缓存*/
(0)
public class BlossomPropertySourceLocator implements PropertySourceLocator {private BlossomConfigManager manager;private BlossomConfigProperties properties;//使用builder的方式得到来自各种地方的BlossomPropertySource//最后将BlossomPropertySource放入到CompositePropertySource即可private BlossomPropertySourceBuilder blossomPropertySourceBuilder;public BlossomPropertySourceLocator(BlossomConfigManager manager) {this.manager = manager;this.properties = manager.getProperties();} public PropertySource<?> locate(Environment environment) {this.properties.setEnvironment(environment);ConfigService configService = manager.getConfigService();if (Objects.isNull(configService)) {log.warn("No instance of ConfigService was found,can not load config from ConfigService");return null;}this.blossomPropertySourceBuilder = new BlossomPropertySourceBuilder(configService, properties);CompositePropertySource ps = new CompositePropertySource(BlossomConstants.BLOSSOM_PROPERTY_SOURCE_NAME);loadApplicationConfig(ps);loadConfigLists(ps);return ps;}/*** 加载项目所有配置* @param ps*/private void loadConfigLists(CompositePropertySource ps) {List<BlossomConfigProperties.BlossomConfig> configLists = this.properties.getConfigLists();configLists.forEach(config -> {loadConfigIfPresent(ps, config.getConfigId(), config.getGroup(), this.properties.getFileExtension());});}/*** 在项目项目原生配置* @param ps*/private void loadApplicationConfig(CompositePropertySource ps) {Environment env = this.properties.getEnvironment();String applicationName = env.getProperty("spring.application.name");String group = this.properties.getGroup();String fileExtension = this.properties.getFileExtension();for (String profile : env.getActiveProfiles()) {//blossom-core-dev.yamlString configId = applicationName + SEPARATOR + profile + DOT + this.properties.getFileExtension();loadConfigIfPresent(ps, configId, group, fileExtension);}}private void loadConfigIfPresent(CompositePropertySource ps, String configId, String group, String fileExtension) {if (StringUtils.isBlank(configId)) {return;}if (StringUtils.isBlank(group)) {return;}Boolean validType = ConfigType.isValidType(fileExtension);//the file extension is unvalid;if (!validType) {return;}this.loadBlossomConfig(ps, configId, group, fileExtension);}private void loadBlossomConfig(CompositePropertySource ps, String configId, String group, String fileExtension) {//1:从配置中心获取配置 并且封装为BlossomPropertySourceBlossomPropertySource blossomPropertySource =this.blossomPropertySourceBuilder.buildBlossomPropertySource(configId, group, fileExtension);//2:将配置转换为PropertySource ---能得到Properties类型即可//3:将配置添加到CompositePropertySourceps.addFirstPropertySource(blossomPropertySource);}}
然后在代码中按照我们所说的,完成对配置中心配置的加载和获取。
下面的代码,才是真正的连接到配置中心,并且对配置中心的配置进行拉取以及解析的地方。
那么到此,我们就已经完成了项目启动的时候的初始化属性的配置了。
接下来我们要思考一下,如何在项目的配置文件发生变更的时候,能刷新本地的配置呢?
package blossom.project.config.core;import blossom.project.config.client.ConfigService;
import blossom.project.config.common.exception.BlossomException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.env.PropertySource;import java.util.Collections;
import java.util.Date;
import java.util.List;/*** @author: ZhangBlossom* @date: 2023/12/29 20:16* @contact: QQ:4602197553* @contact: WX:qczjhczs0114* @blog: https://blog.csdn.net/Zhangsama1* @github: https://github.com/ZhangBlossom* BlossomPropertySourceBuilder类*/
public class BlossomPropertySourceBuilder {private ConfigService configService;private BlossomConfigProperties properties;public BlossomPropertySourceBuilder(ConfigService configService, BlossomConfigProperties properties) {this.configService = configService;this.properties = properties;}/*** 当前方法会完成BlossomPropertySource的构建** @param configId* @param group* @param fileExtension* @return*/BlossomPropertySource buildBlossomPropertySource(String configId, String group, String fileExtension) {//从配置中心得到配置并且封装为List类型的PropertySourceList<PropertySource<?>> propertySources = loadBlossomConfigData(configId, group, fileExtension);//将List转换为最后我们需要的BlossomPropertySourceBlossomPropertySource blossomPropertySource = new BlossomPropertySource(group, configId, new Date(),propertySources);return blossomPropertySource;}/*** 当前方法完成对配置中心配置的加载和解析* 并且最终返回PropertySource集合** @param configId* @param group* @param fileExtension* @return*/private List<PropertySource<?>> loadBlossomConfigData(String configId, String group, String fileExtension) {List<PropertySource<?>> propertySources = Collections.emptyList();try {//得到配置文件的内容String configData = this.configService.getConfig(configId, group, fileExtension);if (StringUtils.isBlank(configData)) {log.warn("the data from ConfigService is empty, configId: {}, group:{}", configId, group);return Collections.emptyList();}//在spring中想要将配置解析为PropertySource可以用自带的解析器--只提供了yaml和properties//也就是说json/xml等其他格式需要自己实现propertySources =BlossomConfigDataHandler.getInstance().parseConfigData(configId, configData, fileExtension);} catch (BlossomException e) {log.error("get the config data from ConfigService failed, configId: {}, group:{},Exception:{}", configId,group, e);} catch (Exception e) {log.error("parse the config data failed. Exception:{}", e);}return propertySources;}}
我们知道,其实对于配置变更事件,两种实现方法,一种push,一种pull。
push就是让Server端主动的通知Client,实现起来相比pull更加复杂,因此这里我们选择使用pull的方式,也就是让CLient主动的去Server端拉取配置变更。
那么这里就会用到我们上面所说的长轮询了,以及我们的事件发布机制。
因为我们得让Client这个普通的Java项目能通知道Core模块这个Spring项目,并且让我们轻松的利用到Spring项目中提供的强大的事件监听机制。
重点就是,Client模块我说到的Publish接口。
在Core模块中
BlossomConfigChangeEventSubscriber blossomConfigChangeEventSubscriber(BlossomConfigManager manager,BlossomConfigChangePublisher publisher){return new BlossomConfigChangeEventSubscriber(manager.getConfigService(),publisher);
}
public
有如下的一个类,这个类的作用就是在Core模块启动的时候,主动的根据配置中心的信息,去发起一个长轮询监听请求。
class BlossomConfigChangeEventSubscriber {private ConfigService configService;private Properties properties;private Publish publish;public BlossomConfigChangeEventSubscriber(ConfigService configService, BlossomConfigChangePublisher publisher) {this.configService = configService;this.properties = configService.getProperties();this.publish = publisher;} public void listen() {//得到当前项目所有生效的配置List<BlossomConfigProperties.BlossomConfig> configLists =(List<BlossomConfigProperties.BlossomConfig>) this.properties.get(BlossomConfigPropertiesKeyConstants.CONFIG_LISTS);if (configLists.isEmpty()) {return;}//对这些配置进行遍历,为他们添加监听器//使得这些配置发生变更之后我能监听到对应的事件 从而对这些事件进行处理configLists.stream().forEach(config -> {this.configService.subscribeConfigChangeEvent(config.getGroup(),config.getConfigId(),this.publish);});}}
public
而这里的subscribeConfigChangeEvent就是Client端实现的代码。
那么这里,我们想要让Core项目能知道配置变更,那么我们只要确保我们的Core模块提供的Publish实现能发送事件即可。
public class BlossomConfigChangePublisher extends AbstractConfigChangePublish {private final ApplicationEventPublisher applicationEventPublisher;public BlossomConfigChangePublisher(ApplicationEventPublisher applicationEventPublisher) {this.applicationEventPublisher = applicationEventPublisher;} public void publishRemoveEvent(String key) {BlossomConfigChangeEvent event = new BlossomConfigChangeEvent(this, key);applicationEventPublisher.publishEvent(event);} public void publishPublishEvent(String key, ConfigCache configCache) {BlossomConfigChangeEvent event = new BlossomConfigChangeEvent(this, key, configCache);applicationEventPublisher.publishEvent(event);}
}
这样子,一旦我们的Core模块能监听到事件,那么如何刷新@Value注解对应的值,其实就简单了,我们在文章开篇就已经讲解了。
这里我们来看看监听器的实现即可。
package blossom.project.config.core;import blossom.project.config.common.exception.BlossomException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.Collections;
import java.util.List;import static blossom.project.config.common.constants.BlossomConstants.SEPARATOR;/*** @author: ZhangBlossom* @date: 2023/12/30 22:49* @contact: QQ:4602197553* @contact: WX:qczjhczs0114* @blog: https://blog.csdn.net/Zhangsama1* @github: https://github.com/ZhangBlossom*/
public class BlossomConfigChangeListener implements ApplicationListener<BlossomConfigChangeEvent> {private static final String REFRESH_SCOPE = "RefreshScope"; private ConfigurableApplicationContext applicationContext;private BeanDefinitionRegistry beanDefinitionRegistry; private Environment environment; public void init() {ScopeRegistry scopeRegistry = applicationContext.getBean(ScopeRegistry.class);this.beanDefinitionRegistry = scopeRegistry.getBeanDefinitionRegistry();} public void onApplicationEvent(BlossomConfigChangeEvent event) {// 处理配置更改事件if (event.getConfigCache() != null) {// 处理发布事件System.out.println("Config published: " + event.getKey());doPublishEvent(event);} else {// 处理删除事件System.out.println("Config removed: " + event.getKey());doRemoveEvent(event);}}/*** 处理配置变更事件** @param event*/private void doPublishEvent(BlossomConfigChangeEvent event) {//1:根据event中的key 找到对应的配置String key = event.getKey();//2:根据新的content信息,解析完毕之后,重新添加到Environemnt中String content = event.getConfigCache().getContent();//得到文件解析格式String type = event.getConfigCache().getType();List<PropertySource<?>> newPropertySources = Collections.emptyList();//得到配置文件的内容if (StringUtils.isBlank(content)) {log.warn("the data from ConfigService is empty, key:{}", key);return;}//在spring中想要将配置解析为PropertySource可以用自带的解析器--只提供了yaml和properties//也就是说json/xml等其他格式需要自己实现String configId = parseKey(key);try {newPropertySources =BlossomConfigDataHandler.getInstance().parseConfigData(configId, content, type);} catch (IOException e) {throw new RuntimeException(e);}// 将新的PropertySource添加到Environment中for (PropertySource<?> propertySource : newPropertySources) {((ConfigurableEnvironment) environment).getPropertySources().addFirst(propertySource);}// 触发环境变更事件,以刷新@Value注解的值applicationContext.publishEvent(new EnvironmentChangeEvent(applicationContext, Collections.singleton(event.getKey())));// 刷新带有RefreshScope注解的BeanrefreshScopedBeans();}/*** 刷新@Value注解的值*/private void refreshScopedBeans() {String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();for (String beanDefinitionName : beanDefinitionNames) {BeanDefinition beanDefinition = beanDefinitionRegistry.getBeanDefinition(beanDefinitionName);if (REFRESH_SCOPE.equalsIgnoreCase(beanDefinition.getScope())) {applicationContext.getBeanFactory().destroyScopedBean(beanDefinitionName);applicationContext.getBean(beanDefinitionName);}}}/*** 处理配置删除事件** @param event*/private void doRemoveEvent(BlossomConfigChangeEvent event) {//1:删除对应的Environment//2: 不刷新@Value}/*** 根据key返回configid* @param key* @return*/private String parseKey(String key){return key.substring(key.lastIndexOf(SEPARATOR));}}
在上面的Listener代码中,我们就顺利的完成了Core模块对Client模块的整合,完成了变更事件的监听以及变更事件的处理。
代码比较好理解,不做过多的解释了。
其实,完成上面的代码之后,一个非常简易的配置中心就做完了,上面的代码已经可以完成配置的加载和变更了。
如果代码只是写到这里,那么这个项目也只是类似于一个Demo,帮助我们了解Spring与配置中心的关系。
但是亮点并不多,只能说帮助你和面试官聊天的时候聊到这一块有一些说辞。
所以,我打算在上面的版本中,进行一下简单的优化,用上一些”花里胡哨“的功能。