ETCD
作为云原生的一大基础项目,其在k8s中的应用让它得到了很大关注,自己当时在学习k8s的时候也就简单的了解一下,并没有过多关注,后来学习raft算法又对其产生了兴趣。都说其可以作为服务注册中心和配置中心使用,但是目前好像并没有看到有什么java项目使用ETCD
,也可能是spring cloud整体生态比较成熟,不管是配置中心还是注册中心都有很多优秀的解决方案。但是基于好奇心我还是准备摸索以下如何使用ETCD
作为配置中心,这里只是做一个基础的demo,不会太深入。
一、ETCD
首先我们需要一个ETCD
服务端,我直接本地启动,因为我之前本地搭建ETCD
集群本地有部署过,本次就直接使用单节点启动。另外为了更方便查看数据,我IDEA
安装了EtcdHelper
插件。
启动使用默认配置,然后通过etcdctl
创建角色、用户,并通过key前缀给角色授权。这些可以参考官方文档。这里我简单说下我在项目中会使用到的一些配置信息。
端口:2379,角色:admin
,并给这个角色授予key前缀权限/spring_etcd/、/etcd_demo/、/etcd_config/
,用户etcdAdmin
,密码:123456
,这些配置在项目中会用到,这里就先介绍一下。
二、config server
首先我们新建一个spring boot项目。引入相关的依赖,这里额外引入了JPA
和Security
,项目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>
项目的配置文件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地址、用户名、密码、命名空间(本项目没用上)。接下来我们根据配置文件创建一个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();
}
}
这里主要是根据endpoint、user、password
创建了一个Client
,这里都要根据ByteSequence
来转换感觉不是很方便。这里其实我开始是准备用namespace
的,但是自己对这个用法还不是很了解,而且文档也没找到对应的内容,所以暂时就没用上。我理解应该就是一个key
或者前缀,这个等我有时间再来研究一下吧。
接下来就是很关键的一步,就是提供一个供客户端获取配置文件的接口。通过spring cloud config文档我们知道config server
其实是通过http
为外部提供配置信息的,如果想使用自定义的存储,其实只要我们实现EnvironmentRepository
接口即可,当然也要实现Ordered
并重写getOrdered
方法,不然的话我们的配置优先级默认会是最低的,因此我们新创建一个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();
}
}
因为在配置类里面我将order
的值设为最小值,这样我们整个配置的优先级最高。简单的解释一下代码,首先就是根据客户端的应用名称和配置文件名称作为key前缀,查询所有的key-value键值对,并将这些键值对放入配置文件Map
中,然后将其放入到Environment
中。这里我当时测试了以下application、profile
具体的值,这里我们可以等会debug
看下。
好了到这里我们的server
端已经完成,接下来我们创建client
端项目。
三 client server
同上,这里我们创建一个springf clound config client
项目,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
的依赖,我的目的很简单,就是数据库的用户名、密码这些敏感信息从config server
获取。然后通过一个简单的查询接口保证从server
端获取到正确的配置信息。
client
项目的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
的默认配置。这里我们指向server
端地址即可。通过上面的配置文件也发现我没有配置数据库用户名和密码。
另外我写了两个简单的接口用于测试,代码如下:
@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("动态配置的值:{}", configValue);
return configValue;
}
}
一个是查询用户,一个是获取通过@Value
注入的配置信息。等会我们通过接口分别测试以下两个接口能否按照期望返回相应的结果。
四 测试
先后启动server
和client
项目,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.
没有从远端获取到PropertySource
,因为server
端引入了spring security
这个好像是默认开启的,所以先注掉依赖,如果要开启的话一定要在server
端和client
端都配置好Security
用户名和密码,不然client
端通过http
接口无法获取到Environment
。再次启动server
和client
,这个时候server
端的断点生效了,如下图:
通过上图我们可以看到application
变量值就是spring.cloud.config.name
配置的值,而profile
则是默认配置文件default
,但是我们实际配置的spring.profiles.active
值是dev
,跳过断点,如下图:
通过两次
debug
断点我们发现client
端获取配置文件是根据config.name
+ profile
来获取的,所以我们可以在Etcd
中设置好我们需要的配置信息,这也是为什么一开始我就要给admin
角色按照key前缀授权的原因。我们通过EtcdHelper
新增几项配置,如下图:在
EtcdEnvironmentRepository
类中,我们是根据application
和profile
组合,作为key前缀的,然后根据从Etcd
返回的key、value组装成我们最终需要的配置信息。client
启动成功,如下图:最后我们通过测试接口测试一下能否达到我们期望的结果。
根据id查询用户信息,接口如下:
查询
@Value
注解注入的值,如下:都没有问题,说明client
从server
端获取的配置是正确的,关于client
查询配置可以看下ConfigServerConfigDataLoader
这个类的源码。
五 最后
关于使用Etcd
做为配置中心的内容先到这里,但是在这次学习中我也发现了一个问题就是配置如何自动刷新,我尝试了一下没有成功,这个等我有时间再研究研究,如果成功了我再来单独开一篇。本次项目的源码放在我的github。欢迎点赞评论转发~~~~