spring cloud config使用etcd存儲(chǔ)

ETCD作為云原生的一大基礎(chǔ)項(xiàng)目,其在k8s中的應(yīng)用讓它得到了很大關(guān)注,自己當(dāng)時(shí)在學(xué)習(xí)k8s的時(shí)候也就簡單的了解一下,并沒有過多關(guān)注,后來學(xué)習(xí)raft算法又對(duì)其產(chǎn)生了興趣。都說其可以作為服務(wù)注冊(cè)中心和配置中心使用,但是目前好像并沒有看到有什么java項(xiàng)目使用ETCD,也可能是spring cloud整體生態(tài)比較成熟,不管是配置中心還是注冊(cè)中心都有很多優(yōu)秀的解決方案。但是基于好奇心我還是準(zhǔn)備摸索以下如何使用ETCD作為配置中心,這里只是做一個(gè)基礎(chǔ)的demo,不會(huì)太深入。

一、ETCD

首先我們需要一個(gè)ETCD服務(wù)端,我直接本地啟動(dòng),因?yàn)槲抑氨镜卮罱?code>ETCD集群本地有部署過,本次就直接使用單節(jié)點(diǎn)啟動(dòng)。另外為了更方便查看數(shù)據(jù),我IDEA安裝了EtcdHelper插件。
啟動(dòng)使用默認(rèn)配置,然后通過etcdctl創(chuàng)建角色、用戶,并通過key前綴給角色授權(quán)。這些可以參考官方文檔。這里我簡單說下我在項(xiàng)目中會(huì)使用到的一些配置信息。
端口:2379,角色:admin,并給這個(gè)角色授予key前綴權(quán)限/spring_etcd/、/etcd_demo/、/etcd_config/,用戶etcdAdmin,密碼:123456,這些配置在項(xiàng)目中會(huì)用到,這里就先介紹一下。

二、config server

首先我們新建一個(gè)spring boot項(xiàng)目。引入相關(guān)的依賴,這里額外引入了JPASecurity,項(xiàng)目pom.xml如下:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-security</artifactId>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <dependency>
            <groupId>io.etcd</groupId>
            <artifactId>jetcd-core</artifactId>
            <version>0.7.5</version>
        </dependency>

項(xiàng)目的配置文件application.properties如下:

spring.application.name=spring-etcd
server.port=9090
spring.profiles.active=etcd

## mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true
spring.jpa.database=mysql
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=none

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useSSL=false&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456

spring.cloud.config.server.etcd.endpoints=http://127.0.0.1:2379
spring.cloud.config.server.etcd.user-name=etcdAdmin
spring.cloud.config.server.etcd.password=123456
#spring.cloud.config.server.etcd.name-space=/spring_etcd/

logging.level.root=info

接下來我們新增一個(gè)ETCD配置類EtcdConfigProperties,代碼如下:

@Data
@Configuration
@ConfigurationProperties(prefix = "spring.cloud.config.server.etcd")
public class EtcdConfigProperties implements EnvironmentRepositoryProperties {

    private int order = Ordered.LOWEST_PRECEDENCE;

    private List<String> endpoints;

    private boolean enable;

    private String nameSpace;

    private String userName;

    private String password;

    @Override
    public void setOrder(int order) {
        this.order = order;
    }
}

這里主要定義了ETCD的endpoint地址、用戶名、密碼、命名空間(本項(xiàng)目沒用上)。接下來我們根據(jù)配置文件創(chuàng)建一個(gè)Client,代碼如下:

@Configuration
@ConditionalOnBean({EtcdConfigProperties.class})
public class EtcdClientConfig {


    @Bean
    public Client createClient(EtcdConfigProperties etcdConfigProperties) {
        List<String> endpoints = etcdConfigProperties.getEndpoints();

        return Client.builder().endpoints(endpoints.toArray(new String[endpoints.size()]))
//                .namespace(ByteSequence.from(etcdConfigProperties.getNameSpace(), StandardCharsets.UTF_8))
                .user(ByteSequence.from(etcdConfigProperties.getUserName(),StandardCharsets.UTF_8))
                .password(ByteSequence.from(etcdConfigProperties.getPassword(), StandardCharsets.UTF_8)).build();
    }
}

這里主要是根據(jù)endpoint、user、password創(chuàng)建了一個(gè)Client,這里都要根據(jù)ByteSequence來轉(zhuǎn)換感覺不是很方便。這里其實(shí)我開始是準(zhǔn)備用namespace的,但是自己對(duì)這個(gè)用法還不是很了解,而且文檔也沒找到對(duì)應(yīng)的內(nèi)容,所以暫時(shí)就沒用上。我理解應(yīng)該就是一個(gè)key或者前綴,這個(gè)等我有時(shí)間再來研究一下吧。
接下來就是很關(guān)鍵的一步,就是提供一個(gè)供客戶端獲取配置文件的接口。通過spring cloud config文檔我們知道config server其實(shí)是通過http為外部提供配置信息的,如果想使用自定義的存儲(chǔ),其實(shí)只要我們實(shí)現(xiàn)EnvironmentRepository接口即可,當(dāng)然也要實(shí)現(xiàn)Ordered并重寫getOrdered方法,不然的話我們的配置優(yōu)先級(jí)默認(rèn)會(huì)是最低的,因此我們新創(chuàng)建一個(gè)EtcdEnvironmentRepository類,代碼如下:

@Slf4j
@Configuration
@Profile("etcd")
public class EtcdEnvironmentRepository implements EnvironmentRepository, Ordered {

    private final Client client;

    private final EtcdConfigProperties configProperties;

    public EtcdEnvironmentRepository(Client client, EtcdConfigProperties configProperties) {
        this.client = client;
        this.configProperties = configProperties;
    }

    @SneakyThrows
    @Override
    public Environment findOne(String application, String profile, String label) {
        String queryKey = "/" + application + "/" + profile;
        GetOption option = GetOption.newBuilder().isPrefix(true).build();
        GetResponse response = client.getKVClient().get(ByteSequence.from(queryKey, StandardCharsets.UTF_8), option).get();
        List<KeyValue> list = response.getKvs();
        // 配置文件
        Map<String, String> properties = new HashMap<>();
        for (KeyValue kv : response.getKvs()) {
            String key = kv.getKey().toString(StandardCharsets.UTF_8);
            String value = kv.getValue().toString(StandardCharsets.UTF_8);
            key = key.replace(queryKey, "").replace("/", "");
            log.info("key={} ,value={}", key, value);
            properties.put(key, value);
        }
        Environment environment = new Environment(application, profile);
        //
        String propertyName = application + "-" + profile + ".properties";
        environment.add(new PropertySource(propertyName, properties));
        propertyName = "application-" + profile + ".properties";
//        environment.add(new PropertySource(propertyName, properties));

//        environment.add(new PropertySource(application + ".properties", properties));
        return environment;
    }

    @Override
    public Environment findOne(String application, String profile, String label, boolean includeOrigin) {
        return EnvironmentRepository.super.findOne(application, profile, label, includeOrigin);
    }

    @Override
    public int getOrder() {
        return configProperties.getOrder();
    }
}

因?yàn)樵谂渲妙惱锩嫖覍?code>order的值設(shè)為最小值,這樣我們整個(gè)配置的優(yōu)先級(jí)最高。簡單的解釋一下代碼,首先就是根據(jù)客戶端的應(yīng)用名稱和配置文件名稱作為key前綴,查詢所有的key-value鍵值對(duì),并將這些鍵值對(duì)放入配置文件Map中,然后將其放入到Environment中。這里我當(dāng)時(shí)測(cè)試了以下application、profile具體的值,這里我們可以等會(huì)debug看下。
好了到這里我們的server端已經(jīng)完成,接下來我們創(chuàng)建client端項(xiàng)目。

三 client server

同上,這里我們創(chuàng)建一個(gè)springf clound config client項(xiàng)目,pom.xml引入依賴如下:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

這里除了引入spring-cloud-starter-config還引入了JPA、mysql的依賴,我的目的很簡單,就是數(shù)據(jù)庫的用戶名、密碼這些敏感信息從config server獲取。然后通過一個(gè)簡單的查詢接口保證從server端獲取到正確的配置信息。
client項(xiàng)目的application.properties如下:

spring.application.name=etcd_demo
server.port=6060
spring.profiles.active=dev
spring.config.import=optional:configserver:http://127.0.0.1:9090
# config server user and password
#spring.cloud.config.username=etcdAdmin
#spring.cloud.config.password=123456
# config name default value application.name
spring.cloud.config.name=etcd_config
spring.cloud.refresh.enabled=true

spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true
spring.jpa.database=mysql
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=none

## mysql properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useSSL=false&characterEncoding=utf8&allowPublicKeyRetrieval=true

這里注意以下就是spring.config.import的使用,這個(gè)是Spring Boot 2.4 引入的一種新的引入配置文件的方式,它是綁定到Config Server的默認(rèn)配置。這里我們指向server端地址即可。通過上面的配置文件也發(fā)現(xiàn)我沒有配置數(shù)據(jù)庫用戶名和密碼。
另外我寫了兩個(gè)簡單的接口用于測(cè)試,代碼如下:

@Slf4j
@Service
public class UserServiceImpl implements IUserService {


    @Value("${dynamic.value}")
    private String configValue;

    private final UserRepository userRepository;

    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserEntity queryById(Integer id) {
        return userRepository.findById(id).get();
    }

    @Override
    public String queryConfig() {
        log.info("動(dòng)態(tài)配置的值:{}", configValue);
        return configValue;
    }
}

一個(gè)是查詢用戶,一個(gè)是獲取通過@Value注入的配置信息。等會(huì)我們通過接口分別測(cè)試以下兩個(gè)接口能否按照期望返回相應(yīng)的結(jié)果。

四 測(cè)試

先后啟動(dòng)serverclient項(xiàng)目,server端啟動(dòng)成功,client啟動(dòng)出錯(cuò)了,信息如下:

2024-09-03T21:23:12.922+08:00  INFO 15273 --- [etcd_demo] [           main] c.y.s.etcdclient.EtcdClientApplication   : The following 1 profile is active: "dev"
2024-09-03T21:23:12.964+08:00  INFO 15273 --- [etcd_demo] [           main] o.s.c.c.c.ConfigServerConfigDataLoader   : Fetching config from server at : http://127.0.0.1:9090
2024-09-03T21:23:12.964+08:00  WARN 15273 --- [etcd_demo] [           main] o.s.c.c.c.ConfigServerConfigDataLoader   : Could not locate PropertySource ([ConfigServerConfigDataResource@6c451c9c uris = array<String>['http://127.0.0.1:9090'], optional = true, profiles = 'default']): Could not extract response: no suitable HttpMessageConverter found for response type [class org.springframework.cloud.config.environment.Environment] and content type [text/html;charset=UTF-8]
2024-09-03T21:23:12.964+08:00  INFO 15273 --- [etcd_demo] [           main] o.s.c.c.c.ConfigServerConfigDataLoader   : Fetching config from server at : http://127.0.0.1:9090
2024-09-03T21:23:12.965+08:00  WARN 15273 --- [etcd_demo] [           main] o.s.c.c.c.ConfigServerConfigDataLoader   : Could not locate PropertySource ([ConfigServerConfigDataResource@cc62a3b uris = array<String>['http://127.0.0.1:9090'], optional = true, profiles = 'dev']): Could not extract response: no suitable HttpMessageConverter found for response type [class org.springframework.cloud.config.environment.Environment] and content type [text/html;charset=UTF-8]
2024-09-03T21:23:12.965+08:00  INFO 15273 --- [etcd_demo] [           main] o.s.c.c.c.ConfigServerConfigDataLoader   : Fetching config from server at : http://127.0.0.1:9090
2024-09-03T21:23:12.965+08:00  WARN 15273 --- [etcd_demo] [           main] o.s.c.c.c.ConfigServerConfigDataLoader   : Could not locate PropertySource ([ConfigServerConfigDataResource@6cc0bcf6 uris = array<String>['http://127.0.0.1:9090'], optional = true, profiles = 'default']): Could not extract response: no suitable HttpMessageConverter found for response type [class org.springframework.cloud.config.environment.Environment] and content type [text/html;charset=UTF-8]
2024-09-03T21:23:13.521+08:00  INFO 15273 --- [etcd_demo] [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.

沒有從遠(yuǎn)端獲取到PropertySource,因?yàn)?code>server端引入了spring security這個(gè)好像是默認(rèn)開啟的,所以先注掉依賴,如果要開啟的話一定要在server端和client端都配置好Security用戶名和密碼,不然client端通過http接口無法獲取到Environment。再次啟動(dòng)serverclient,這個(gè)時(shí)候server端的斷點(diǎn)生效了,如下圖:

截圖 2024-09-03 21-37-47.png

通過上圖我們可以看到application變量值就是spring.cloud.config.name配置的值,而profile則是默認(rèn)配置文件default,但是我們實(shí)際配置的spring.profiles.active值是dev,跳過斷點(diǎn),如下圖:

截圖 2024-09-03 21-48-24.png

通過兩次debug斷點(diǎn)我們發(fā)現(xiàn)client端獲取配置文件是根據(jù)config.name + profile來獲取的,所以我們可以在Etcd中設(shè)置好我們需要的配置信息,這也是為什么一開始我就要給admin角色按照key前綴授權(quán)的原因。我們通過EtcdHelper新增幾項(xiàng)配置,如下圖:
截圖 2024-09-03 21-53-43.png

EtcdEnvironmentRepository類中,我們是根據(jù)applicationprofile組合,作為key前綴的,然后根據(jù)從Etcd返回的key、value組裝成我們最終需要的配置信息。
client啟動(dòng)成功,如下圖:
截圖 2024-09-03 21-58-56.png

最后我們通過測(cè)試接口測(cè)試一下能否達(dá)到我們期望的結(jié)果。
根據(jù)id查詢用戶信息,接口如下:
截圖 2024-09-03 22-01-10.png

查詢@Value注解注入的值,如下:
截圖 2024-09-03 22-01-26.png

都沒有問題,說明clientserver端獲取的配置是正確的,關(guān)于client查詢配置可以看下ConfigServerConfigDataLoader這個(gè)類的源碼。

五 最后

關(guān)于使用Etcd做為配置中心的內(nèi)容先到這里,但是在這次學(xué)習(xí)中我也發(fā)現(xiàn)了一個(gè)問題就是配置如何自動(dòng)刷新,我嘗試了一下沒有成功,這個(gè)等我有時(shí)間再研究研究,如果成功了我再來單獨(dú)開一篇。本次項(xiàng)目的源碼放在我的github。歡迎點(diǎn)贊評(píng)論轉(zhuǎn)發(fā)~~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容