Zookeepers_docker workdir

Zookeepers_docker workdir文章目录Curator客户端创建会话创建节点获取节点和数据更新数据删除节点事务节点存在事件监听其他工具类开发测试Curator客户端Curator包含了几个包:curator-framework:对zookeeper的底层api的一些封装curator-client:提供一些客户端的操作,例如重试策略等curator-recipes:封装了一些高级特性,如:Cache事件监听、选举…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

Curator客户端

  1. Curator包含了几个包:
    • curator-framework:对zookeeper的底层api的一些封装
    • curator-client:提供一些客户端的操作,例如重试策略等
    • curator-recipes:封装了一些高级特性,如:Cache事件监听、选举、分布式锁、分布式计数器、分布式Barrier等
  2. 版本
    • The are currently two released versions of Curator, 2.x.x and 3.x.x:
    • Curator 2.x.x – compatible with both ZooKeeper 3.4.x and ZooKeeper 3.5.x
    • Curator 3.x.x – compatible only with ZooKeeper 3.5.x and includes support for new features such as dynamic reconfiguration, etc.
  3. Curator主要解决了以下问题
    • 封装ZooKeeper clientZooKeeper server之间的连接处理
    • 提供了一套Fluent风格的操作API
    • 提供ZooKeeper各种应用场景(recipe, 比如共享锁服务, 集群领导选举机制)的抽象封装
  4. 这里使用的版本
    • 服务端Zookeeper版本:3.4.6
    • Curator版本:compile "org.apache.curator:curator-recipes:2.11.0"

创建会话

  1. 创建会话

     @Test public void testCreateClient() throws Exception{ 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; /** * 失去连接重试策略 * baseSleepTimeMs 初始sleep时间 * maxRetries 最大重试次数 * maxSleepMs 最大sleep时间 * 当前sleep时间=baseSleepTimeMs*Math.mac(1,random.next(1)) << (retryCount+1) */ int baseSleepTimeMs; int maxRetries; int maxSleepMs; RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); //第一种方式,使用Builder fluent风格的API方式创建 CuratorFramework curatorFramework1 = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .build(); CuratorFrameworkState state1 = curatorFramework1.getState(); //zk-curator - LATENT logger.info(state1.toString()); //当连接状态变化时触发 curatorFramework1.getConnectionStateListenable().addListener(new ConnectionStateListener() { 
           @Override public void stateChanged(CuratorFramework curatorFramework, ConnectionState connectionState) { 
           //连接状态事件:true logger.info("连接状态事件:{}",connectionState.isConnected()); } }); //完成会话的创建 curatorFramework1.start(); //第二种方式,使用静态工厂方式创建,此方式本质上还是通过第一种方式创建 CuratorFramework curatorFramework2 = CuratorFrameworkFactory.newClient( connectString, sessionTimeoutMs, connectionTimeoutMs, retryPolicy); CuratorFrameworkState state2 = curatorFramework2.getState(); //zk-curator - LATENT logger.info(state2.toString()); /** * 临时客户端: * 一定时间不活动后连接会被关闭,使用buildTemp()创建 */ CuratorTempFramework curatorFramewor3 = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .buildTemp(2000,TimeUnit.SECONDS); //.buildTemp(); TimeUnit.SECONDS.sleep(20); } 

    Jetbrains全家桶1年46,售后保障稳定

创建节点

  1. 创建节点

     @Test public void testCreateZnode() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "jannal-create"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework curatorFramework = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) //客户端对zk上的数据节点的操作都是基本此根节点进行的 .build(); //完成会话创建 curatorFramework.start(); //创建一个初始内容为空的znode,默认创建的是持久节点 curatorFramework.create().forPath("/userEmpty"); //创建一个附带内容的znode curatorFramework.create().forPath("/userData", "我是jannal".getBytes(Charsets.UTF_8)); //创建一个临时节点 curatorFramework.create(). withMode(CreateMode.EPHEMERAL). forPath("/userTemp", "jannal".getBytes(Charsets.UTF_8)); //自动递归创建父节点creatingParentsIfNeeded() curatorFramework.create() .creatingParentsIfNeeded() .withMode(CreateMode.PERSISTENT) .forPath("/user/password", "123456".getBytes(Charsets.UTF_8)); curatorFramework.getCuratorListenable().addListener(new CuratorListener() { 
           @Override public void eventReceived(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception { 
           } }); ExecutorService executorService = Executors.newFixedThreadPool(2); CountDownLatch countDownLatch = new CountDownLatch(1); /** * 在原生的zookeeper客户端中,所有异步通知事件处理都是通过EventThread这个 * 线程处理的(串行处理所有事件的通知)。一旦发生复杂的处理单元就会消耗很长事件 * 从而影响其他事件的处理,Curator可以使用线程池来处理,如果不指定线程池 * 则默认使用EventThread来处理 */ curatorFramework.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL) .inBackground(new BackgroundCallback() { 
           @Override public void processResult(CuratorFramework client, CuratorEvent event) throws Exception { 
           logger.info(event.toString()); countDownLatch.countDown(); } },executorService) .forPath("/user2","jannal2".getBytes(Charsets.UTF_8)); countDownLatch.await(); executorService.shutdown(); } 

获取节点和数据

  1. 获取节点和数据

     @Test public void testGetZnode() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "jannal-get"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) .build(); client.start(); client.create() .creatingParentsIfNeeded() .forPath("/user/username/jannal", "jannal123".getBytes(Charsets.UTF_8)); //获取数据 byte[] bytes = client.getData().forPath("/user/username/jannal"); Assert.assertEquals("jannal123", new String(bytes, Charsets.UTF_8)); //读取一个节点的数据内容,同时获取到该节点的stat Stat stat = new Stat(); bytes = client.getData().storingStatIn(stat).forPath("/user/username/jannal"); logger.info(ToStringBuilder.reflectionToString(stat)); client.create().creatingParentsIfNeeded().forPath("/user/username1", "abc".getBytes(Charsets.UTF_8)); client.create().creatingParentsIfNeeded().forPath("/user/username2", "def".getBytes(Charsets.UTF_8)); //获取节点的子节点路径 List<String> strings = client.getChildren().forPath("/user"); //[username2, username1, username] logger.info(strings.toString()); } 

更新数据

  1. 更新数据

     @Test public void testSetZnode() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "jannal-update"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework curatorFramework = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) //客户端对zk上的数据节点的操作都是基本此根节点进行的 .build(); curatorFramework.start(); String path = "/user/password"; curatorFramework.create() .creatingParentsIfNeeded() .withMode(CreateMode.PERSISTENT) .forPath("/user/password", "123456".getBytes(Charsets.UTF_8)); byte[] bytes = curatorFramework.getData().forPath(path); Assert.assertEquals("123456", new String(bytes, Charsets.UTF_8)); Stat stat = curatorFramework.setData().forPath(path, "abcd".getBytes(Charsets.UTF_8)); bytes = curatorFramework.getData().forPath(path); Assert.assertEquals("abcd", new String(bytes, Charsets.UTF_8)); //更新一个节点的数据内容,强制指定版本进行更新 curatorFramework.setData().withVersion(stat.getVersion()).forPath(path, "abcde".getBytes()); } 

删除节点

  1. 在删除一个节点时,由于网络原因,导致删除操作失败。这个异常在有些场景下是致命的,比如Master选举(先创建后删除),针对这个问题Curator引入了一种重试机制,即如果我们调用了guaranteed(),那么只要客户端有效,就会在后台反复重试,直到节点删除成功。

  2. 删除节点示例

     @Test public void testDeleteZnode() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "jannal-delete"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) .build(); client.start(); client.create() .creatingParentsIfNeeded() .forPath("/user/username/jannal", "jannal123".getBytes(Charsets.UTF_8)); client.create() .creatingParentsIfNeeded() .forPath("/user2/username/jannal", "jannal123".getBytes(Charsets.UTF_8)); client.create() .creatingParentsIfNeeded() .forPath("/user3/username/jannal", "jannal123".getBytes(Charsets.UTF_8)); //删除节点且递归删除其所有的子节点 client.delete().deletingChildrenIfNeeded().forPath("/user"); //强制指定版本进行删除 client.delete().deletingChildrenIfNeeded().withVersion(0).forPath("/user2"); //强制保证删除,只要客户端有效,Curator会在后台持续进行删除操作,直到节点删除成功 client.delete().guaranteed().forPath("/user3/username/jannal"); } 

事务

  1. 事务

     /** * inTransaction()开启事务,可以复合create, setData, check, and/or delete * 等操作然后调用commit()作为一个原子操作提交 * * @throws Exception */ @Test public void testTransaction() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "jannal-transaction"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) .build(); client.start(); client.inTransaction() .create().withMode(CreateMode.EPHEMERAL).forPath("/transaction", "事务".getBytes(Charsets.UTF_8)) .and() .delete().forPath("/transaction") .and() .commit(); } 

节点存在

  1. 判断节点是否存在

     @Test public void testExist() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "jannal-Exist"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) .build(); client.start(); //节点不存在返回null Stat stat = client.checkExists().forPath("/jannal"); Assert.assertEquals(null, stat); logger.info(ToStringBuilder.reflectionToString(stat)); } 

事件监听

  1. 原生客户端支持通过注册Watch来进行事件监听,但是收到事件通知后每次都需要手动再次注册Watch。Curator引入了Cache来实现对Zookeeper服务端事件的监听,并且自动处理反复注册监听,简化了原生API的繁琐。

  2. Cache分为两类监听

    • 节点监听
    • 子节点监听
  3. NodeCache用于监听指定Zookeeper数据节点本身的变化,数据内容发生变化时,就会回调此方法。

     @Test public void testNodeCacheEventListener() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "jannal-listener"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) .build(); client.start(); String path = "/user/username/jannal"; client.create() .creatingParentsIfNeeded() .forPath(path, "jannal123".getBytes(Charsets.UTF_8)); //是否进行数据压缩 boolean dataIsCompressed = false; final NodeCache cache = new NodeCache(client, path, dataIsCompressed); cache.start(true); CountDownLatch countDownLatch = new CountDownLatch(2); cache.getListenable().addListener(new NodeCacheListener() { 
           @Override public void nodeChanged() throws Exception { 
           ChildData currentData = cache.getCurrentData(); if (currentData != null) { 
           logger.info("数据发生变更,新数据:{}", new String(currentData.getData(), StandardCharsets.UTF_8)); }else{ 
           logger.info("数据发生变更,新数据:{},节点可能被删除", currentData, StandardCharsets.UTF_8); } countDownLatch.countDown(); } }); client.setData().forPath(path, "jannal".getBytes(Charsets.UTF_8)); client.delete().deletingChildrenIfNeeded().forPath(path); countDownLatch.await(); } 
  4. PathChildrenCache用于监听指定Zookeeper数据节点的子节点变化情况。当指定节点的子节点发生变化时,就会回调Listener。Curator无法对二级子节点触发变更事件。比如对/zk-jannal子节点进行监听,当/zk-jannal/user/password节点被创建或者删除的时候,无法触发子节点变更事件

     @Test public void testPathChildrenCacheEventListener() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "jannal-parent-listener"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) .build(); client.start(); String path = "/user"; client.create() .creatingParentsIfNeeded() .forPath(path, "jannal123".getBytes(Charsets.UTF_8)); //是否进行数据压缩 boolean dataIsCompressed = false; /** * 是否把节点内容缓存起来,如果配置为true,则client在接收 * 到节点列表变更的同时,也能够获取到节点的数据内容,如果为false * 则无法获取节点的数据内容 */ boolean cacheData = true; ExecutorService executorService = Executors.newFixedThreadPool(2); CountDownLatch countDownLatch = new CountDownLatch(3); final PathChildrenCache cache = new PathChildrenCache(client, path, cacheData, dataIsCompressed, executorService); /** * BUILD_INITIAL_CACHE 同步初始化客户端的cache,及创建cache后,就从服务器端拉入对应的数据(这个是NodeCache使用的方式) * NORMAL 异步初始化cache * POST_INITIALIZED_EVENT 异步初始化,初始化完成触发事件PathChildrenCacheEvent.Type.INITIALIZED */ cache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); cache.getListenable().addListener(new PathChildrenCacheListener() { 
           @Override public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception { 
           ChildData currentData = pathChildrenCacheEvent.getData(); //事件类型,可通过判断事件类型来做相应的处理 PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType(); switch (type) { 
           //初始化会触发这个事件 case INITIALIZED: logger.info("子类缓存系统初始化完成"); break; case CHILD_ADDED: //新增子节点/user/username logger.info("新增子节点{}", currentData.getPath()); break; case CHILD_UPDATED: // 子节点数据变更[106, 97, 110, 110, 97, 108] logger.info("子节点数据变更{}", new String(currentData.getData(),Charsets.UTF_8)); break; case CHILD_REMOVED: //子节点数据删除/user/username logger.info("子节点数据删除{}", currentData.getPath()); break; } countDownLatch.countDown(); } }); client.create() .creatingParentsIfNeeded() .forPath(path+"/username", "jannal123".getBytes(Charsets.UTF_8)); TimeUnit.SECONDS.sleep(1); client.setData().forPath(path+"/username", "jannal".getBytes(Charsets.UTF_8)); TimeUnit.SECONDS.sleep(1); client.delete().deletingChildrenIfNeeded().forPath(path); countDownLatch.await(); } 
  5. TreeCache可以监听整个树上的所有节点。即可以监控节点的状态,还监控节点的子节点的状态

     @Test public void testTreeCache() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "jannal-tree-listener"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) .build(); client.start(); String path = "/user"; client.create() .creatingParentsIfNeeded() .forPath(path, "jannal123".getBytes(Charsets.UTF_8)); CountDownLatch countDownLatch = new CountDownLatch(1); TreeCache cache = new TreeCache(client, path); cache.getListenable().addListener(new TreeCacheListener() { 
           @Override public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception { 
           TreeCacheEvent.Type type = treeCacheEvent.getType(); ChildData childData = treeCacheEvent.getData(); switch (type) { 
           //子类缓存系统初始化完成 case INITIALIZED: logger.info("子类缓存系统初始化完成"); break; case NODE_ADDED: //新增子节点/user/username logger.info("新增子节点{}", childData != null ? childData.getPath() : null); break; case NODE_UPDATED: // 子节点数据变更jannal // 子节点数据变更jannal2 logger.info("子节点数据变更{}", childData != null ? new String(childData.getData(), Charsets.UTF_8) : null); break; case NODE_REMOVED: //子节点数据删除/user logger.info("子节点数据删除{}", childData != null ? childData.getPath() : null); break; } countDownLatch.countDown(); } }); cache.start(); client.setData().forPath(path, "jannal".getBytes()); Thread.sleep(1000); client.setData().forPath(path, "jannal2".getBytes()); Thread.sleep(1000); client.delete().deletingChildrenIfNeeded().forPath(path); Thread.sleep(1000 * 2); countDownLatch.await(); } 

其他工具类

  1. Curator提供了一些工具类。比如ZKPathsEnsurePath

  2. ZKPaths提供更简单的API来构建Znode路径、递归创建和删除节点。

  3. EnsurePath提供了一种确保数据节点存在的机制。在分布式环境下,A和B机器都同时创建节点,由于并发操作的存在,可能抛出节点已经存在的异常EnsurePath内部实现就是试图创建指定节点,如果节点存在,那么就不进行任何操作,也不抛出异常,否则正常创建节点。从Curator 2.9.0版本开始此类已经过时,首选 CuratorFramework.create().creatingParentContainersIfNeeded() or CuratorFramework.exists().creatingParentContainersIfNeeded()

  4. 代码示例

     @Test public void testOther() throws Exception { 
           String connectString = "zk-master:2180,zk-slave1:2182,zk-slave2:2183"; int sessionTimeoutMs = 25000; int connectionTimeoutMs = 5000; String rootPath = "zk-other"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(4000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectString) .sessionTimeoutMs(sessionTimeoutMs) .connectionTimeoutMs(connectionTimeoutMs) .retryPolicy(retryPolicy) .namespace(rootPath) .build(); client.start(); String path = ZKPaths.fixForNamespace(rootPath, "/user"); // 输出:/zk-other/user logger.info(path); ZKPaths.PathAndNode pathAndNode = ZKPaths.getPathAndNode("/zk-other/user/username"); // path:/zk-other/user,node:username logger.info("path:{},node:{}", pathAndNode.getPath(), pathAndNode.getNode()); CuratorZookeeperClient zookeeperClient = client.getZookeeperClient(); //创建节点 ZKPaths.mkdirs(zookeeperClient.getZooKeeper(), "/zk-other/user/password"); //获取子节点 List<String> sortedChildren = ZKPaths.getSortedChildren(zookeeperClient.getZooKeeper(), "/zk-other"); logger.info(sortedChildren.toString()); } 

开发测试

  1. 为了便于开发人员进行zk的开发与测试,可以使用curator-test模块。依赖compile "org.apache.curator:curator-test:2.11.0"

  2. 单元测试

     @Test public void testSingleServer() throws Exception { 
           File dataDir = new File("."); TestingServer server = new TestingServer(2000, dataDir); server.start(); CuratorFramework curatorFramework = CuratorFrameworkFactory. builder(). connectString(server.getConnectString()). sessionTimeoutMs(1000). retryPolicy(new RetryNTimes(3, 1000)). build(); curatorFramework.start(); System.out.println(curatorFramework.getChildren().forPath("/")); curatorFramework.close(); server.stop(); } 
  3. 集群测试

     @Test public void testClusterServer() throws Exception { 
           TestingCluster server = new TestingCluster(3); server.start(); Thread.sleep(2000); TestingZooKeeperServer leader = null; for (TestingZooKeeperServer zs : server.getServers()) { 
           logger.info(zs.getInstanceSpec().getServerId() + "-"); logger.info(zs.getQuorumPeer().getServerState() + "-"); logger.info(zs.getInstanceSpec().getDataDirectory().getAbsolutePath()); if (zs.getQuorumPeer().getServerState().equals("leading")) { 
           leader = zs; } } //模拟leader挂掉 leader.kill(); logger.info("leader 挂掉之后"); for (TestingZooKeeperServer zs : server.getServers()) { 
           logger.info(zs.getInstanceSpec().getServerId() + "-"); logger.info(zs.getQuorumPeer().getServerState() + "-"); logger.info(zs.getInstanceSpec().getDataDirectory().getAbsolutePath()); } server.stop(); } 
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/206666.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号