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)的依賴,這里額外引入了JPA和Security,項(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)server和client項(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)server和client,這個(gè)時(shí)候server端的斷點(diǎn)生效了,如下圖:

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

通過兩次
debug斷點(diǎn)我們發(fā)現(xiàn)client端獲取配置文件是根據(jù)config.name + profile來獲取的,所以我們可以在Etcd中設(shè)置好我們需要的配置信息,這也是為什么一開始我就要給admin角色按照key前綴授權(quán)的原因。我們通過EtcdHelper新增幾項(xiàng)配置,如下圖:
在
EtcdEnvironmentRepository類中,我們是根據(jù)application和profile組合,作為key前綴的,然后根據(jù)從Etcd返回的key、value組裝成我們最終需要的配置信息。client啟動(dòng)成功,如下圖:
最后我們通過測(cè)試接口測(cè)試一下能否達(dá)到我們期望的結(jié)果。
根據(jù)id查詢用戶信息,接口如下:

查詢
@Value注解注入的值,如下:
都沒有問題,說明client從server端獲取的配置是正確的,關(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ā)~~~~