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

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 事件之后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. 重要类

  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;
    }
}
  1. 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();
    }
}
posted @ 2023-03-28 22:24  QiaoZhi  阅读(127)  评论(0编辑  收藏  举报