spring cloud config使用etcd存储

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项目。引入相关的依赖,这里额外引入了JPASecurity,项目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注入的配置信息。等会我们通过接口分别测试以下两个接口能否按照期望返回相应的结果。

四 测试

先后启动serverclient项目,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。再次启动serverclient,这个时候server端的断点生效了,如下图:

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

通过上图我们可以看到application变量值就是spring.cloud.config.name配置的值,而profile则是默认配置文件default,但是我们实际配置的spring.profiles.active值是dev,跳过断点,如下图:

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

通过两次debug断点我们发现client端获取配置文件是根据config.name + profile来获取的,所以我们可以在Etcd中设置好我们需要的配置信息,这也是为什么一开始我就要给admin角色按照key前缀授权的原因。我们通过EtcdHelper新增几项配置,如下图:
截图 2024-09-03 21-53-43.png

EtcdEnvironmentRepository类中,我们是根据applicationprofile组合,作为key前缀的,然后根据从Etcd返回的key、value组装成我们最终需要的配置信息。
client启动成功,如下图:
截图 2024-09-03 21-58-56.png

最后我们通过测试接口测试一下能否达到我们期望的结果。
根据id查询用户信息,接口如下:
截图 2024-09-03 22-01-10.png

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

都没有问题,说明clientserver端获取的配置是正确的,关于client查询配置可以看下ConfigServerConfigDataLoader这个类的源码。

五 最后

关于使用Etcd做为配置中心的内容先到这里,但是在这次学习中我也发现了一个问题就是配置如何自动刷新,我尝试了一下没有成功,这个等我有时间再研究研究,如果成功了我再来单独开一篇。本次项目的源码放在我的github。欢迎点赞评论转发~~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,848评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,529评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,824评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,329评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,227评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,879评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,218评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,877评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,159评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,155评论 2 315
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,987评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,736评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,273评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,407评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,663评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,158评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,517评论 2 339

推荐阅读更多精彩内容