模仿nacos实现自己的配置中心

发布时间 2023-03-28 22:26:57作者: QiaoZhi

0. 配置中心简单交互

  1. 编写自己的sdk:拉取配置、服务器端更新后客户端能感知到并且更新到本地
  2. 和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 事件之后清除缓存,下次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-alg-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>

		<!-- 自己的客户端和服务端通信的sdk -->
    <!-- sdk包含拉取配置,配置更新后能够再次拉取以及发出通知 -->
		
		<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>
	</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拉取配置以及接收变更后的配置。

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 serverAddr;

    private String policyName;

    private String env;

    public String getServerAddr() {
        return serverAddr;
    }

    public void setServerAddr(String serverAddr) {
        this.serverAddr = serverAddr;
    }

    public String getPolicyName() {
        return policyName;
    }

    public void setPolicyName(String policyName) {
        this.policyName = policyName;
    }

    public String getEnv() {
        return env;
    }

    public void setEnv(String env) {
        this.env = env;
    }
}

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 java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class CustomConfigPropertySourceBuilder {

    private ConfigProperties algConfigProperties;

    public CustomConfigPropertySourceBuilder(ConfigProperties algConfigProperties) {
        this.algConfigProperties = algConfigProperties;
    }

    CustomConfigPropertySource build(String dataId, boolean isRefreshable) {
        String policyName = algConfigProperties.getPolicyName();
        Map<String, Object> result = new HashMap<>();
        // todo 调用自己的SDK 拉取配置
        Map<String, String> allConfig = null;
        // 添加到缓存用于refresh
//        ConfigCacheData.getInstance().addConfig(policyName, config);
      
        if (allConfig != null && allConfig.size() > 0) {
            allConfig.forEach((k, v) -> {
                result.put(k, v);
            });
        }
        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;
      // 遍历去获取配置ps对象
//            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. 重要类

  1. 自己的业务listener (监听到数据改变后发布事件)
package com.demo.test.refresh;

import com.xxx.sdk.ConfigNotification;
import com.xxx.sdk.RepositoryChangeListener;
import org.springframework.beans.BeansException;
import org.springframework.cloud.endpoint.event.RefreshEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class CustomConfigChangeListener implements RepositoryChangeListener, ApplicationContextAware {

    private ApplicationContext context;

  	// 相当于client 端监听到server 端数据发生变更
    @Override
    public void onRepositoryChange(String policyName, ConfigNotification configNotification) {
        // 发布Springcloud 事件
        context.publishEvent(new RefreshEvent(this, null, "Refresh config " + policyName));
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

}

  1. Refresher
package com.demo.test.refresh;

import com.demo.test.ConfigProperties;
import com.demo.test.cache.ConfigCacheData;
import com.xxx.sdk.RepositoryChangeListener;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;

import java.util.List;

public class CustomConfigRefresher implements ApplicationListener<ApplicationReadyEvent> {

    private List<RepositoryChangeListener> listeners;

    private ConfigCacheData configCacheData;

    private ConfigProperties algConfigProperties;

    public CustomConfigRefresher(List<RepositoryChangeListener> listeners, ConfigCacheData configCacheData, ConfigProperties algConfigProperties) {
        this.listeners = listeners;
        this.configCacheData = configCacheData;
        this.algConfigProperties = algConfigProperties;
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        configCacheData.addListener(algConfigProperties.getPolicyName(), this.listeners);
    }
}

  1. ConfigCacheData

用于缓存。 实际上相当于SDK拉取的配置和Spring 环境中的对象做的一个关联。用于添加监听器以及发布Spring事件。

2. ConfigRefresherAutoConfiguration 自动配置类

package com.demo.test;

import com.bj58.hy.algorith.platform.config.internals.RepositoryChangeListener;
import com.demo.test.cache.ConfigCacheData;
import com.demo.test.refresh.CustomConfigChangeListener;
import com.demo.test.refresh.CustomConfigRefresher;
import org.springframework.context.annotation.Bean;

import java.util.List;

public class ConfigRefresherAutoConfiguration {

    @Bean
    public CustomConfigChangeListener customConfigChangeListener() {
        return new CustomConfigChangeListener();
    }

    @Bean
    public CustomConfigRefresher customConfigRefresher(List<RepositoryChangeListener> listeners, ConfigProperties algConfigProperties) {
        return new CustomConfigRefresher(listeners, ConfigCacheData.getInstance(), algConfigProperties);
    }

}

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("${manager:''}")
    private String test2;

    /**
     * 测试从environment 获取
     *
     * @return
     */
    @GetMapping("/test")
    public String test() {
        return environment.getProperty("manager");
    }

    /**
     * 测试自动注入
     *
     * @return
     */
    @GetMapping("/test2")
    public String test2() {
        return test2;
    }

    /**
     * 测试 @RefreshScope 更新注解
     *
     * @return
     */
    @GetMapping("/test3")
    public String test3() {
        return this.toString();
    }
}