关于jetcd
- jetcd是etcd v3版本的官方java客户端工具,java项目通过该库可以对etcd执行各种操作,当前最新发布版本是0.5.0
- jetcd官方github:https://github.com/etcd-io/jetcd
- etcd在线api文档:https://etcd.io/docs/next/learning/api/
关于jetcd实战系列
《jetcd实战系列》是欣宸新的原创系列,旨在与大家一起学习如何用jetcd操作etcd,除了基本增删改查,还会涉及到version、监听、租约等etcd特有功能;
系列文章链接
- jetcd实战之一:极速体验
- jetcd实战之二:基本操作
- jetcd实战之三:进阶操作(事务、监听、租约)
本篇概览
作为《jetcd实战系列》的开篇,主要是为整个系列做准备工作,包括以下内容:
- 梳理实战涉及到的应用和库的版本信息;
- 基于docker-compose部署etcd集群;
- 新建gradle工程,作为整个实战系列的父工程;
- 编写helloworld应用,验证jetcd可以正常访问etcd集群;
源码下载
- 本篇实战中的完整源码可在GitHub下载到,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称 | 链接 | 备注 |
---|---|---|
项目主页 | https://github.com/zq2599/blog_demos | 该项目在GitHub上的主页 |
git仓库地址(https) | https://github.com/zq2599/blog_demos.git | 该项目源码的仓库地址,https协议 |
git仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该项目源码的仓库地址,ssh协议 |
- 这个git项目中有多个文件夹,kubebuilder相关的应用在jetcd-tutorials文件夹下,如下图红框所示:
- jetcd-tutorials文件夹下有多个子项目,本篇的是helloworld:
版本信息
实战系列所用的etcd是部署在docker环境下的三个实例组建的集群:
- etcd:3.4.7
- docker:20.10.5(Community)
- docker-compose:1.28.5
- jetcd:0.5.0
- jdk:1.8.0_271
- springboot:2.4.4
- IDEA:2020.2.3 (Ultimate Edition)
- gradle:6.8.3
- 电脑操作系统:macOS Big Sur 11.2.3
部署集群
- 确认docker和docker-compose已正常运行
- 新建docker-compose.yml文件:
version: '3'
services:
etcd1:
image: "quay.io/coreos/etcd:v3.4.7"
entrypoint: /usr/local/bin/etcd
command:
- '--name=etcd1'
- '--data-dir=/etcd_data'
- '--initial-advertise-peer-urls=http://etcd1:2380'
- '--listen-peer-urls=http://0.0.0.0:2380'
- '--listen-client-urls=http://0.0.0.0:2379'
- '--advertise-client-urls=http://etcd1:2379'
- '--initial-cluster-token=etcd-cluster'
- '--heartbeat-interval=250'
- '--election-timeout=1250'
- '--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380'
- '--initial-cluster-state=new'
ports:
- 2379:2379
volumes:
- ./store/etcd1/data:/etcd_data
etcd2:
image: "quay.io/coreos/etcd:v3.4.7"
entrypoint: /usr/local/bin/etcd
command:
- '--name=etcd2'
- '--data-dir=/etcd_data'
- '--initial-advertise-peer-urls=http://etcd2:2380'
- '--listen-peer-urls=http://0.0.0.0:2380'
- '--listen-client-urls=http://0.0.0.0:2379'
- '--advertise-client-urls=http://etcd2:2379'
- '--initial-cluster-token=etcd-cluster'
- '--heartbeat-interval=250'
- '--election-timeout=1250'
- '--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380'
- '--initial-cluster-state=new'
ports:
- 2380:2379
volumes:
- ./store/etcd2/data:/etcd_data
etcd3:
image: "quay.io/coreos/etcd:v3.4.7"
entrypoint: /usr/local/bin/etcd
command:
- '--name=etcd3'
- '--data-dir=/etcd_data'
- '--initial-advertise-peer-urls=http://etcd3:2380'
- '--listen-peer-urls=http://0.0.0.0:2380'
- '--listen-client-urls=http://0.0.0.0:2379'
- '--advertise-client-urls=http://etcd3:2379'
- '--initial-cluster-token=etcd-cluster'
- '--heartbeat-interval=250'
- '--election-timeout=1250'
- '--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380'
- '--initial-cluster-state=new'
ports:
- 2381:2379
volumes:
- ./store/etcd3/data:/etcd_data
- 从上述脚本可见,宿主机的2379、2380、2381三个端口被用来映射三个etcd容器的2379端口;
- 执行命令docker-compose up -d,如下,可见三个容器都已经创建,名称分别是28_etcd1_1、28_etcd2_1、28_etcd3_1,容器名和当前目录名有关:
zhaoqin@zhaoqindeMacBook-Pro-2 28 % docker-compose up -d
Creating network "28_default" with the default driver
Creating 28_etcd2_1 ... done
Creating 28_etcd3_1 ... done
Creating 28_etcd1_1 ... done
- etcd集群已启动成功,来试试基本操作命令是否正常,执行以下命令新建一个键值对,键是/aaa/foo,值是111:
docker exec 28_etcd1_1 /usr/local/bin/etcdctl put /aaa/foo 111
- 查看命令如下(我这换了个容器):
docker exec 28_etcd2_1 /usr/local/bin/etcdctl get /aaa/foo -w fields
- 得到的是完整的结果,除了键值还有Revision、ModRevision、Version、Lease等字段:
"ClusterID" : 10316109323310759371
"MemberID" : 15168875803774599630
"Revision" : 2
"RaftTerm" : 2
"Key" : "/aaa/foo"
"CreateRevision" : 2
"ModRevision" : 2
"Version" : 1
"Value" : "111"
"Lease" : 0
"More" : false
"Count" : 1
新建gradle工程,作为整个实战系列的父工程
- 接下来新建一个gradle工程,整个实战系列都是在此父工程下开发;
- 新建gradle工程名为jetcd-tutorials,其build.gradle内容如下:
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
// gradle自身会用到的相关设置
buildscript {
// 仓库
repositories {
// 本地
mavenLocal()
// 如果有私服就在此配置,如果没有请注释掉
maven {
url 'http://192.168.50.43:8081/repository/aliyun-proxy/'
}
// 阿里云
maven {
url 'http://maven.aliyun.com/nexus/content/groups/public/'
}
// 中央仓库
mavenCentral()
// grandle插件
maven {
url 'https://plugins.gradle.org/m2/'
}
}
// 子模块会用到的变量
ext {
springBootVersion = '2.4.4'
}
}
// 插件
plugins {
id 'java'
id 'java-library'
// 有这个声明,子模块可以使用org.springframework.boot插件而无需指定版本,但是apply=false表示当前模块不使用此插件
id 'org.springframework.boot' version "${springBootVersion}" apply false
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}
// gradle wrapper指定版本
wrapper {
gradleVersion = '6.8.3'
}
// 取当前时间
def buildTimeAndDate = OffsetDateTime.now()
// 根据时间生成字符串变量
ext {
projectVersion = project.version
buildDate = DateTimeFormatter.ISO_LOCAL_DATE.format(buildTimeAndDate)
buildTime = DateTimeFormatter.ofPattern('HH:mm:ss.SSSZ').format(buildTimeAndDate)
}
// 针对所有project的配置,包含根项目
allprojects {
group 'com.bolingcavalry'
version '1.0-SNAPSHOT'
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'io.spring.dependency-management'
// 编译相关参数
compileJava {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
options.encoding = 'UTF-8'
options.compilerArgs = [
'-Xlint:all', '-Xlint:-processing'
]
}
// Copy LICENSE
tasks.withType(Jar) {
from(project.rootDir) {
include 'LICENSE'
into 'META-INF'
}
}
// 生成jar文件时,MANIFEST.MF的内容如下
jar {
manifest {
attributes(
'Created-By': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})".toString(),
'Built-By': 'travis',
'Build-Date': buildDate,
'Build-Time': buildTime,
'Built-OS': "${System.properties['os.name']}",
'Specification-Title': project.name,
'Specification-Version': project.version,
'Specification-Vendor': 'Will Zhao',
'Implementation-Title': project.name,
'Implementation-Version': project.version,
'Implementation-Vendor': 'Will Zhao'
)
}
}
// 仓库
repositories {
// 本地
mavenLocal()
// 如果有私服就在此配置,如果没有请注释掉
maven {
url 'http://192.168.50.43:8081/repository/aliyun-proxy/'
}
// 阿里云
maven {
url 'http://maven.aliyun.com/nexus/content/groups/public/'
}
// 中央仓库
mavenCentral()
// grandle插件
maven {
url "https://plugins.gradle.org/m2/"
}
}
}
// 类似maven的dependencyManagement,这里将所有jar的版本指定好,子模块在依赖时可以不用指定版本
allprojects { project ->
buildscript {
dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-starter-parent:${springBootVersion}"
mavenBom "org.junit:junit-bom:5.7.0"
}
dependencies {
dependency 'org.projectlombok:lombok:1.16.16'
dependency 'org.apache.commons:commons-lang3:3.11'
dependency 'commons-collections:commons-collections:3.2.2'
dependency 'io.etcd:jetcd-core:0.5.0'
dependency 'org.slf4j:slf4j-log4j12:1.7.30'
}
}
}
}
// 坐标信息
group 'com.bolingcavalry'
version '1.0-SNAPSHOT'
- 现在根模块已经建好,后面整个系列的代码都会写在这个根模块下面;
编写helloworld应用
- 接下来写个helloworld应用验证jetcd能不能操作etcd集群;
- 在根模块下面新建名为helloworld的gradle子模块,其build.gradle内容如下:
plugins {
id 'java'
}
// 子模块自己的依赖
dependencies {
implementation 'io.etcd:jetcd-core'
implementation 'org.projectlombok:lombok'
// annotationProcessor不会传递,使用了lombok生成代码的模块,需要自己声明annotationProcessor
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.slf4j:slf4j-log4j12'
testImplementation('org.junit.jupiter:junit-jupiter')
}
test {
useJUnitPlatform()
}
group 'com.bolingcavalry'
version '1.0-SNAPSHOT'
- 新增HelloWorld.java,代码很简单,getKVClient方法用来生成客户端实例,put方法向etcd写入键值对,get方法去etcd查询指定键的值:
package com.bolingcavalry;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.kv.PutResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static com.google.common.base.Charsets.UTF_8;
@Slf4j
public class HelloWorld {
/**
* 新建key-value客户端实例
* @return
*/
private KV getKVClient(){
String endpoints = "http://192.168.50.239:2379,http://192.168.50.239:2380,http://192.168.50.239:2381";
Client client = Client.builder().endpoints(endpoints.split(",")).build();
return client.getKVClient();
}
/**
* 将字符串转为客户端所需的ByteSequence实例
* @param val
* @return
*/
private static ByteSequence bytesOf(String val) {
return ByteSequence.from(val, UTF_8);
}
/**
* 查询指定键对应的值
* @param key
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
public String get(String key) throws ExecutionException, InterruptedException{
log.info("start get, key [{}]", key);
GetResponse response = getKVClient().get(bytesOf(key)).get();
if (response.getKvs().isEmpty()) {
log.error("empty value of key [{}]", key);
return null;
}
String value = response.getKvs().get(0).getValue().toString(UTF_8);
log.info("finish get, key [{}], value [{}]", key, value);
return value;
}
/**
* 创建键值对
* @param key
* @param value
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
public PutResponse put(String key, String value) throws ExecutionException, InterruptedException {
log.info("start put, key [{}], value [{}]", key, value);
return getKVClient().put(bytesOf(key), bytesOf(value)).get();
}
}
- 接下来咱们写个单元测试类来验证上述代码是否有效:
package com.bolingcavalry;
import io.etcd.jetcd.kv.PutResponse;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class HelloWorldTest {
// 用与测试的键
private static final String KEY = "/abc/foo-" System.currentTimeMillis();
// 用于测试的值
private static final String VALUE = "/abc/foo";
@org.junit.jupiter.api.Test
@Order(2)
void get() throws ExecutionException, InterruptedException {
String getResult = new HelloWorld().get(KEY);
assertEquals(VALUE, getResult);
}
@Test
@Order(1)
void put() throws ExecutionException, InterruptedException {
PutResponse putResponse = new HelloWorld().put(KEY, VALUE);
assertNotNull(putResponse);
assertNotNull(putResponse.getHeader());
}
}
- 代码写完后按照下图红框指示执行单元测试,可见jetcd操作etcd成功:
- 至此,《jetcd实战》系列的开篇就完成了,咱们搭建好了etcd集群,还初步体验过jetcd的基本功能,接下来的章节会从基本操作开始,由浅入深的学习jetcd;