ETCD
作為云原生的一大基礎(chǔ)項(xiàng)目,其在k8s中的應(yīng)用讓它得到了很大關(guān)注,自己當(dāng)時在學(xué)習(xí)k8s的時候也就簡單的了解一下捅膘,并沒有過多關(guān)注翘地,后來學(xué)習(xí)raft算法又對其產(chǎn)生了興趣申尤。都說其可以作為服務(wù)注冊中心和配置中心使用,但是目前好像并沒有看到有什么java項(xiàng)目使用ETCD
衙耕,也可能是spring cloud整體生態(tài)比較成熟昧穿,不管是配置中心還是注冊中心都有很多優(yōu)秀的解決方案。但是基于好奇心我還是準(zhǔn)備摸索以下如何使用ETCD
作為配置中心橙喘,這里只是做一個基礎(chǔ)的demo时鸵,不會太深入。
一厅瞎、ETCD
首先我們需要一個ETCD
服務(wù)端饰潜,我直接本地啟動,因?yàn)槲抑氨镜卮罱?code>ETCD集群本地有部署過和簸,本次就直接使用單節(jié)點(diǎn)啟動彭雾。另外為了更方便查看數(shù)據(jù),我IDEA
安裝了EtcdHelper
插件锁保。
啟動使用默認(rèn)配置薯酝,然后通過etcdctl
創(chuàng)建角色、用戶爽柒,并通過key前綴給角色授權(quán)吴菠。這些可以參考官方文檔。這里我簡單說下我在項(xiàng)目中會使用到的一些配置信息霉赡。
端口:2379橄务,角色:admin
,并給這個角色授予key前綴權(quán)限/spring_etcd/穴亏、/etcd_demo/蜂挪、/etcd_config/
重挑,用戶etcdAdmin
,密碼:123456
棠涮,這些配置在項(xiàng)目中會用到谬哀,這里就先介紹一下。
二严肪、config server
首先我們新建一個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
接下來我們新增一個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)建一個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)建了一個Client
竣付,這里都要根據(jù)ByteSequence
來轉(zhuǎn)換感覺不是很方便诡延。這里其實(shí)我開始是準(zhǔn)備用namespace
的,但是自己對這個用法還不是很了解古胆,而且文檔也沒找到對應(yīng)的內(nèi)容肆良,所以暫時就沒用上。我理解應(yīng)該就是一個key
或者前綴赤兴,這個等我有時間再來研究一下吧妖滔。
接下來就是很關(guān)鍵的一步,就是提供一個供客戶端獲取配置文件的接口桶良。通過spring cloud config文檔我們知道config server
其實(shí)是通過http
為外部提供配置信息的,如果想使用自定義的存儲沮翔,其實(shí)只要我們實(shí)現(xiàn)EnvironmentRepository
接口即可陨帆,當(dāng)然也要實(shí)現(xiàn)Ordered
并重寫getOrdered
方法,不然的話我們的配置優(yōu)先級默認(rèn)會是最低的采蚀,因此我們新創(chuàng)建一個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è)為最小值,這樣我們整個配置的優(yōu)先級最高榆鼠。簡單的解釋一下代碼纲爸,首先就是根據(jù)客戶端的應(yīng)用名稱和配置文件名稱作為key前綴,查詢所有的key-value鍵值對妆够,并將這些鍵值對放入配置文件Map
中识啦,然后將其放入到Environment
中负蚊。這里我當(dāng)時測試了以下application、profile
具體的值颓哮,這里我們可以等會debug
看下家妆。
好了到這里我們的server
端已經(jīng)完成,接下來我們創(chuàng)建client
端項(xiàng)目冕茅。
三 client server
同上伤极,這里我們創(chuàng)建一個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
獲取齿税。然后通過一個簡單的查詢接口保證從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
的使用炊豪,這個是Spring Boot 2.4
引入的一種新的引入配置文件的方式凌箕,它是綁定到Config Server
的默認(rèn)配置。這里我們指向server
端地址即可词渤。通過上面的配置文件也發(fā)現(xiàn)我沒有配置數(shù)據(jù)庫用戶名和密碼牵舱。
另外我寫了兩個簡單的接口用于測試,代碼如下:
@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("動態(tài)配置的值:{}", configValue);
return configValue;
}
}
一個是查詢用戶缺虐,一個是獲取通過@Value
注入的配置信息芜壁。等會我們通過接口分別測試以下兩個接口能否按照期望返回相應(yīng)的結(jié)果。
四 測試
先后啟動server
和client
項(xiàng)目高氮,server
端啟動成功慧妄,client
啟動出錯了,信息如下:
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
這個好像是默認(rèn)開啟的塞淹,所以先注掉依賴,如果要開啟的話一定要在server
端和client
端都配置好Security
用戶名和密碼罪裹,不然client
端通過http
接口無法獲取到Environment
饱普。再次啟動server
和client
,這個時候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á)到我們期望的結(jié)果。
根據(jù)id查詢用戶信息呵扛,接口如下:
查詢
@Value
注解注入的值每庆,如下:都沒有問題,說明client
從server
端獲取的配置是正確的今穿,關(guān)于client
查詢配置可以看下ConfigServerConfigDataLoader
這個類的源碼缤灵。
五 最后
關(guān)于使用Etcd
做為配置中心的內(nèi)容先到這里,但是在這次學(xué)習(xí)中我也發(fā)現(xiàn)了一個問題就是配置如何自動刷新蓝晒,我嘗試了一下沒有成功腮出,這個等我有時間再研究研究,如果成功了我再來單獨(dú)開一篇芝薇。本次項(xiàng)目的源碼放在我的github胚嘲。歡迎點(diǎn)贊評論轉(zhuǎn)發(fā)~~~~