|
@@ -1,273 +1,300 @@
|
|
|
package com.bfkj.unidia.IOUtils;
|
|
|
|
|
|
+import com.bfkj.unidia.DataUtils.DataFormatConverter;
|
|
|
+import com.bfkj.unidia.Result;
|
|
|
+import com.github.benmanes.caffeine.cache.Cache;
|
|
|
import org.apache.rocketmq.client.apis.ClientConfiguration;
|
|
|
import org.apache.rocketmq.client.apis.ClientException;
|
|
|
import org.apache.rocketmq.client.apis.ClientServiceProvider;
|
|
|
-import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
|
|
|
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
|
|
|
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
|
|
|
import org.apache.rocketmq.client.apis.consumer.PushConsumer;
|
|
|
import org.apache.rocketmq.client.apis.message.Message;
|
|
|
-import org.apache.rocketmq.client.apis.message.MessageBuilder;
|
|
|
import org.apache.rocketmq.client.apis.message.MessageView;
|
|
|
import org.apache.rocketmq.client.apis.producer.Producer;
|
|
|
import org.apache.rocketmq.client.apis.producer.SendReceipt;
|
|
|
-import org.apache.rocketmq.client.apis.producer.Transaction;
|
|
|
-import org.apache.rocketmq.client.apis.producer.TransactionResolution;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
|
-import java.io.Closeable;
|
|
|
-import java.time.Duration;
|
|
|
-import java.util.Map;
|
|
|
-import java.util.concurrent.CompletableFuture;
|
|
|
-import java.util.concurrent.ConcurrentHashMap;
|
|
|
-import java.util.concurrent.ExecutorService;
|
|
|
-import java.util.concurrent.Executors;
|
|
|
+import java.util.*;
|
|
|
import java.util.function.Consumer;
|
|
|
|
|
|
-public class RocketMQHelper implements Closeable {
|
|
|
+import static com.bfkj.unidia.cacheUtils.CacheUtil.buildCaffeineCache;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class RocketMQHelper {
|
|
|
private static final Logger logger = LoggerFactory.getLogger(RocketMQHelper.class);
|
|
|
- // RocketMQ 客户端服务提供者
|
|
|
private static final ClientServiceProvider PROVIDER = ClientServiceProvider.loadService();
|
|
|
|
|
|
- // 生产者缓存 (producerGroup -> Producer)
|
|
|
- private static final Map<String, Producer> PRODUCER_MAP = new ConcurrentHashMap<>();
|
|
|
+ // 缓存实例
|
|
|
+ private static final Cache<String, Producer> producerCache = buildCaffeineCache();
|
|
|
+ private static final Cache<String, PushConsumer> consumerCache = buildCaffeineCache();
|
|
|
|
|
|
- // 消费者缓存 (consumerGroup -> PushConsumer)
|
|
|
- private static final Map<String, PushConsumer> CONSUMER_MAP = new ConcurrentHashMap<>();
|
|
|
+ // 锁对象
|
|
|
+ private static final Object producerLock = new Object();
|
|
|
+ private static final Object consumerLock = new Object();
|
|
|
|
|
|
- // 线程池用于异步操作
|
|
|
- private static final ExecutorService ASYNC_EXECUTOR = Executors.newFixedThreadPool(
|
|
|
- Runtime.getRuntime().availableProcessors() * 4
|
|
|
- );
|
|
|
- private RocketMQHelper(){}
|
|
|
- // 默认客户端配置
|
|
|
- private static ClientConfiguration defaultClientConfig(String endpoints) {
|
|
|
- return ClientConfiguration.newBuilder()
|
|
|
- .setEndpoints(endpoints)
|
|
|
- .setRequestTimeout(Duration.ofSeconds(3))
|
|
|
- .build();
|
|
|
- }
|
|
|
- // 获取或创建生产者
|
|
|
- private static synchronized Producer getOrCreateProducer(String producerGroup, String endpoints) {
|
|
|
- return PRODUCER_MAP.computeIfAbsent(producerGroup, key -> {
|
|
|
- try {
|
|
|
- ClientConfiguration config = defaultClientConfig(endpoints);
|
|
|
- return PROVIDER.newProducerBuilder()
|
|
|
- .setClientConfiguration(config)
|
|
|
- .build();
|
|
|
- } catch (ClientException e) {
|
|
|
- throw new RocketMQException("Failed to create producer: " + producerGroup, e);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- // 获取或创建消费者
|
|
|
- private static synchronized PushConsumer getOrCreateConsumer(String consumerGroup, String endpoints,
|
|
|
- String topic, String tag, Consumer<MessageView> messageHandler) throws ClientException {
|
|
|
- String cacheKey = consumerGroup + "@" + topic + "#" + tag;
|
|
|
- return CONSUMER_MAP.computeIfAbsent(cacheKey, key -> {
|
|
|
- try {
|
|
|
- ClientConfiguration config = defaultClientConfig(endpoints);
|
|
|
- FilterExpression filter = new FilterExpression(tag, FilterExpressionType.TAG);
|
|
|
+ private RocketMQHelper() {}
|
|
|
|
|
|
- return PROVIDER.newPushConsumerBuilder()
|
|
|
- .setClientConfiguration(config)
|
|
|
- .setConsumerGroup(consumerGroup)
|
|
|
- .setSubscriptionExpressions(Map.of(topic, filter))
|
|
|
- .setMessageListener(messageView -> {
|
|
|
- messageHandler.accept(messageView);
|
|
|
- return ConsumeResult.SUCCESS;
|
|
|
- })
|
|
|
- .build();
|
|
|
- } catch (ClientException e) {
|
|
|
- throw new RocketMQException("Failed to create consumer: " + consumerGroup, e);
|
|
|
- }
|
|
|
- });
|
|
|
+ /**
|
|
|
+ * 发送消息到指定主题,支持单条或批量发送。
|
|
|
+ *
|
|
|
+ * @param config 配置信息,包含端点、生产者组等
|
|
|
+ * @param topic 主题名称
|
|
|
+ * @param tag 标签
|
|
|
+ * @param data 待发送的消息对象,可以是单个对象或对象列表
|
|
|
+ * @return Result<List<?>> 操作结果,包含发送成功的消息数量
|
|
|
+ */
|
|
|
+ public static Result<List<?>> send(Map<String, String> config,
|
|
|
+ String topic,
|
|
|
+ String tag,
|
|
|
+ Object data) {
|
|
|
+ return batchSendMessage(config, topic, tag,
|
|
|
+ data instanceof List<?> dataList ? dataList : Collections.singletonList(data));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 同步发送消息
|
|
|
+ * 批量发送消息到 RocketMQ
|
|
|
*
|
|
|
- * @param producerGroup 生产者组
|
|
|
- * @param endpoints RocketMQ 端点
|
|
|
- * @param topic 主题
|
|
|
- * @param tag 标签
|
|
|
- * @param body 消息体
|
|
|
- * @param properties 消息属性
|
|
|
- * @return 发送回执
|
|
|
+ * @param config RocketMQ 连接配置(必须包含 endpoints 和 producerGroup)
|
|
|
+ * @param topic 主题名称
|
|
|
+ * @param tag 标签
|
|
|
+ * @param messages 待发送的消息列表
|
|
|
+ * @return Result<List<?>> 发送结果
|
|
|
*/
|
|
|
- public static SendReceipt sendSync(String producerGroup, String endpoints,
|
|
|
- String topic, String tag, byte[] body, Map<String, String> properties)
|
|
|
- throws ClientException {
|
|
|
- Producer producer = getOrCreateProducer(producerGroup, endpoints);
|
|
|
-
|
|
|
- MessageBuilder builder = PROVIDER.newMessageBuilder()
|
|
|
- .setTopic(topic)
|
|
|
- .setTag(tag)
|
|
|
- .setBody(body);
|
|
|
-
|
|
|
- if (properties != null) {
|
|
|
- properties.forEach(builder::addProperty);
|
|
|
+ private static Result<List<?>> batchSendMessage(Map<String, String> config,
|
|
|
+ String topic,
|
|
|
+ String tag,
|
|
|
+ List<?> messages) {
|
|
|
+ Producer producer = null;
|
|
|
+ List<String> failList = new ArrayList<>();
|
|
|
+ try {
|
|
|
+ // 校验配置参数有效性
|
|
|
+ Result<String> validateResult = validateConfig(config, topic, tag, messages);
|
|
|
+ if (!validateResult.isSuccess()) {
|
|
|
+ return Result.fail(validateResult.getError());
|
|
|
+ }
|
|
|
+ // 获取生产者
|
|
|
+ producer = getProducer(config);
|
|
|
+ if (producer == null) {
|
|
|
+ return Result.fail("获取生产者失败");
|
|
|
+ }
|
|
|
+ for (Object messageObj : messages) {
|
|
|
+ try {
|
|
|
+ byte[] body = DataFormatConverter.convertObjectToString(messageObj).getBytes(); // 将消息对象转换为字节数组
|
|
|
+ SendReceipt receipt = producer.send(buildMessage(topic, tag, body));
|
|
|
+ logger.info("消息发送成功: {}", receipt.getMessageId());
|
|
|
+ } catch (ClientException e) {
|
|
|
+ logger.error("发送消息失败", e);
|
|
|
+ failList.add(messageObj.toString());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Result.success(failList);
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("RocketMQ 批量发送异常", e);
|
|
|
+ return Result.fail("RocketMQ 批量发送异常: " + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ if (producer != null) {
|
|
|
+ try {
|
|
|
+ producer.close();
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("关闭生产者失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- Message message = builder.build();
|
|
|
- return producer.send(message);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 异步发送消息
|
|
|
+ * 启动消费者
|
|
|
*
|
|
|
- * @param producerGroup 生产者组
|
|
|
- * @param endpoints RocketMQ 端点
|
|
|
- * @param topic 主题
|
|
|
- * @param tag 标签
|
|
|
- * @param body 消息体
|
|
|
- * @param properties 消息属性
|
|
|
- * @return CompletableFuture 发送结果
|
|
|
+ * @param config 配置信息,包含端点、消费者组等
|
|
|
+ * @param topic 主题名称
|
|
|
+ * @param tag 标签
|
|
|
+ * @param messageHandler 消息处理函数
|
|
|
+ * @return Result<String> 操作结果
|
|
|
*/
|
|
|
- public static CompletableFuture<SendReceipt> sendAsync(String producerGroup, String endpoints,
|
|
|
- String topic, String tag, byte[] body, Map<String, String> properties) {
|
|
|
- return CompletableFuture.supplyAsync(() -> {
|
|
|
- try {
|
|
|
- return sendSync(producerGroup, endpoints, topic, tag, body, properties);
|
|
|
- } catch (ClientException e) {
|
|
|
- throw new RocketMQException("Async send failed", e);
|
|
|
+ public static Result<String> startConsumer(Map<String, String> config,
|
|
|
+ String topic,
|
|
|
+ String tag,
|
|
|
+ Consumer<MessageView> messageHandler) {
|
|
|
+ try {
|
|
|
+ // 校验配置参数有效性
|
|
|
+ Result<String> validateResult = validateConfig(config, topic, tag, null);
|
|
|
+ if (!validateResult.isSuccess()) {
|
|
|
+ return Result.fail(validateResult.getError());
|
|
|
}
|
|
|
- }, ASYNC_EXECUTOR);
|
|
|
+ // 获取消费者
|
|
|
+ PushConsumer consumer = getConsumer(config, topic, tag, messageHandler);
|
|
|
+ if (consumer == null) {
|
|
|
+ return Result.fail("获取消费者失败");
|
|
|
+ }
|
|
|
+ return Result.success("消费者启动成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("启动消费者失败", e);
|
|
|
+ return Result.fail("启动消费者失败: " + e.getMessage());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 发送单向消息(不关心发送结果)
|
|
|
+ * 获取或创建生产者
|
|
|
*
|
|
|
- * @param producerGroup 生产者组
|
|
|
- * @param endpoints RocketMQ 端点
|
|
|
- * @param topic 主题
|
|
|
- * @param tag 标签
|
|
|
- * @param body 消息体
|
|
|
- * @param properties 消息属性
|
|
|
+ * @param config RocketMQ 配置信息
|
|
|
+ * @return Producer 生产者实例
|
|
|
*/
|
|
|
- public static void sendOneway(String producerGroup, String endpoints,
|
|
|
- String topic, String tag, byte[] body, Map<String, String> properties) {
|
|
|
- ASYNC_EXECUTOR.execute(() -> {
|
|
|
+ private static Producer getProducer(Map<String, String> config) {
|
|
|
+ String cacheKey = generateProducerKey(config);
|
|
|
+ // 尝试从缓存中获取已存在的生产者
|
|
|
+ Producer cached = producerCache.getIfPresent(cacheKey);
|
|
|
+ if (cached != null) {
|
|
|
+ return cached;
|
|
|
+ }
|
|
|
+ // 使用双重检查锁定确保线程安全
|
|
|
+ synchronized (producerLock) {
|
|
|
+ // 再次检查缓存避免重复创建
|
|
|
+ cached = producerCache.getIfPresent(cacheKey);
|
|
|
+ if (cached != null) {
|
|
|
+ return cached;
|
|
|
+ }
|
|
|
try {
|
|
|
- sendSync(producerGroup, endpoints, topic, tag, body, properties);
|
|
|
+ ClientConfiguration clientConfig = defaultClientConfig(config.get("endpoints"));
|
|
|
+ Producer producer = PROVIDER.newProducerBuilder()
|
|
|
+ .setClientConfiguration(clientConfig)
|
|
|
+ .build();
|
|
|
+ producerCache.put(cacheKey, producer);
|
|
|
+ return producer;
|
|
|
} catch (ClientException e) {
|
|
|
- // 记录日志但不抛出异常
|
|
|
- logger.error("无确认机制消息发送异常: " + e.getMessage());
|
|
|
+ logger.error("创建生产者失败", e);
|
|
|
+ return null;
|
|
|
}
|
|
|
- });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 发送事务消息
|
|
|
+ * 获取或创建消费者
|
|
|
*
|
|
|
- * @param producerGroup 生产者组
|
|
|
- * @param endpoints RocketMQ 端点
|
|
|
- * @param topic 主题
|
|
|
- * @param tag 标签
|
|
|
- * @param body 消息体
|
|
|
- * @param properties 消息属性
|
|
|
- * @param checker 事务检查器
|
|
|
- * @return 发送回执
|
|
|
+ * @param config RocketMQ 配置信息
|
|
|
+ * @param topic 主题名称
|
|
|
+ * @param tag 标签
|
|
|
+ * @param messageHandler 消息处理函数
|
|
|
+ * @return PushConsumer 消费者实例
|
|
|
*/
|
|
|
- public static SendReceipt sendTransaction(String producerGroup, String endpoints,
|
|
|
- String topic, String tag, byte[] body, Map<String, String> properties,
|
|
|
- TransactionChecker checker) throws ClientException {
|
|
|
- Producer producer = getOrCreateProducer(producerGroup, endpoints);
|
|
|
-
|
|
|
- MessageBuilder builder = PROVIDER.newMessageBuilder()
|
|
|
- .setTopic(topic)
|
|
|
- .setTag(tag)
|
|
|
- .setBody(body);
|
|
|
-
|
|
|
- if (properties != null) {
|
|
|
- properties.forEach(builder::addProperty);
|
|
|
+ private static PushConsumer getConsumer(Map<String, String> config,
|
|
|
+ String topic,
|
|
|
+ String tag,
|
|
|
+ Consumer<MessageView> messageHandler) throws ClientException {
|
|
|
+ String cacheKey = generateConsumerKey(config, topic, tag);
|
|
|
+ // 尝试从缓存中获取已存在的消费者
|
|
|
+ PushConsumer cached = consumerCache.getIfPresent(cacheKey);
|
|
|
+ if (cached != null) {
|
|
|
+ return cached;
|
|
|
}
|
|
|
-
|
|
|
- Message message = builder.build();
|
|
|
-
|
|
|
- // 开始事务
|
|
|
- Transaction tx = producer.beginTransaction();
|
|
|
- try {
|
|
|
- SendReceipt receipt = producer.send(message, tx);
|
|
|
- // 执行本地事务逻辑 (通过checker实现)
|
|
|
- TransactionResolution resolution = checker.check(message);
|
|
|
-
|
|
|
- if (resolution == TransactionResolution.COMMIT) {
|
|
|
- tx.commit();
|
|
|
- } else {
|
|
|
- tx.rollback();
|
|
|
+ synchronized (consumerLock) {
|
|
|
+ // 再次检查缓存避免重复创建
|
|
|
+ cached = consumerCache.getIfPresent(cacheKey);
|
|
|
+ if (cached != null) {
|
|
|
+ return cached;
|
|
|
}
|
|
|
- return receipt;
|
|
|
- } catch (Exception e) {
|
|
|
- tx.rollback();
|
|
|
- throw new RocketMQException("事务消息失败", e);
|
|
|
+ ClientConfiguration clientConfig = defaultClientConfig(config.get("endpoints"));
|
|
|
+ FilterExpression filter = new FilterExpression(tag, FilterExpressionType.TAG);
|
|
|
+ PushConsumer consumer = PROVIDER.newPushConsumerBuilder()
|
|
|
+ .setClientConfiguration(clientConfig)
|
|
|
+ .setConsumerGroup(config.get("consumerGroup"))
|
|
|
+ .setSubscriptionExpressions(Map.of(topic, filter))
|
|
|
+ .setMessageListener(messageView -> {
|
|
|
+ messageHandler.accept(messageView);
|
|
|
+ return org.apache.rocketmq.client.apis.consumer.ConsumeResult.SUCCESS;
|
|
|
+ })
|
|
|
+ .build();
|
|
|
+ // 不需要显式调用start(),PushConsumerBuilder.build()方法会自动启动消费者
|
|
|
+ consumerCache.put(cacheKey, consumer);
|
|
|
+ return consumer;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 启动消费者
|
|
|
+ * 配置验证
|
|
|
*
|
|
|
- * @param consumerGroup 消费者组
|
|
|
- * @param endpoints RocketMQ 端点
|
|
|
- * @param topic 主题
|
|
|
- * @param tag 标签
|
|
|
- * @param messageHandler 消息处理函数
|
|
|
+ * @param config RocketMQ 配置信息
|
|
|
+ * @param topic 主题名称
|
|
|
+ * @param tag 标签
|
|
|
+ * @param messages 待发送的消息列表
|
|
|
+ * @return Result<String> 验证结果
|
|
|
*/
|
|
|
- public static void startConsumer(String consumerGroup, String endpoints,
|
|
|
- String topic, String tag, Consumer<MessageView> messageHandler)
|
|
|
- throws ClientException {
|
|
|
- getOrCreateConsumer(consumerGroup, endpoints, topic, tag, messageHandler);
|
|
|
+ private static Result<String> validateConfig(Map<String, String> config,
|
|
|
+ String topic,
|
|
|
+ String tag,
|
|
|
+ List<?> messages) {
|
|
|
+ if (messages != null && messages.isEmpty()) {
|
|
|
+ return Result.fail("消息列表为空");
|
|
|
+ }
|
|
|
+ if (topic == null || topic.isEmpty()) {
|
|
|
+ return Result.fail("主题名称不能为空");
|
|
|
+ }
|
|
|
+ if (tag == null || tag.isEmpty()) {
|
|
|
+ return Result.fail("标签不能为空");
|
|
|
+ }
|
|
|
+ if (config == null || !config.containsKey("endpoints") || config.get("endpoints").isEmpty()) {
|
|
|
+ return Result.fail("配置中缺少 endpoints");
|
|
|
+ }
|
|
|
+ if (!config.containsKey("producerGroup") || config.get("producerGroup").isEmpty()) {
|
|
|
+ return Result.fail("配置中缺少 producerGroup");
|
|
|
+ }
|
|
|
+ return Result.success("验证通过");
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 关闭资源
|
|
|
+ * 构建消息
|
|
|
+ *
|
|
|
+ * @param topic 主题名称
|
|
|
+ * @param tag 标签
|
|
|
+ * @param body 消息体
|
|
|
+ * @return 构建好的消息
|
|
|
*/
|
|
|
- @Override
|
|
|
- public void close() {
|
|
|
- // 关闭所有生产者
|
|
|
- PRODUCER_MAP.forEach((group, producer) -> {
|
|
|
- try {
|
|
|
- producer.close();
|
|
|
- } catch (Exception e) {
|
|
|
- logger.error("生产者关闭失败 " + group + ": " + e.getMessage());
|
|
|
- }
|
|
|
- });
|
|
|
- PRODUCER_MAP.clear();
|
|
|
-
|
|
|
- // 关闭所有消费者
|
|
|
- CONSUMER_MAP.forEach((key, consumer) -> {
|
|
|
- try {
|
|
|
- consumer.close();
|
|
|
- } catch (Exception e) {
|
|
|
- logger.error("消费者关闭失败 " + key + ": " + e.getMessage());
|
|
|
- }
|
|
|
- });
|
|
|
- CONSUMER_MAP.clear();
|
|
|
-
|
|
|
- // 关闭线程池
|
|
|
- ASYNC_EXECUTOR.shutdown();
|
|
|
+ private static Message buildMessage(String topic,
|
|
|
+ String tag,
|
|
|
+ byte[] body) {
|
|
|
+ return PROVIDER.newMessageBuilder()
|
|
|
+ .setTopic(topic)
|
|
|
+ .setTag(tag)
|
|
|
+ .setBody(body)
|
|
|
+ .build();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 自定义 RocketMQ 异常
|
|
|
+ * 生成生产者缓存键
|
|
|
+ *
|
|
|
+ * @param config RocketMQ 配置信息
|
|
|
+ * @return 缓存键
|
|
|
*/
|
|
|
- public static class RocketMQException extends RuntimeException {
|
|
|
- public RocketMQException(String message) {
|
|
|
- super(message);
|
|
|
- }
|
|
|
+ private static String generateProducerKey(Map<String, String> config) {
|
|
|
+ return String.join(":", config.get("endpoints"), config.get("producerGroup"));
|
|
|
+ }
|
|
|
|
|
|
- public RocketMQException(String message, Throwable cause) {
|
|
|
- super(message, cause);
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * 生成消费者缓存键
|
|
|
+ *
|
|
|
+ * @param config RocketMQ 配置信息
|
|
|
+ * @param topic 主题名称
|
|
|
+ * @param tag 标签
|
|
|
+ * @return 缓存键
|
|
|
+ */
|
|
|
+ private static String generateConsumerKey(Map<String, String> config,
|
|
|
+ String topic,
|
|
|
+ String tag) {
|
|
|
+ return String.join(":", config.get("endpoints"), config.get("consumerGroup"), topic, tag);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 事务检查器接口
|
|
|
+ * 默认客户端配置
|
|
|
+ *
|
|
|
+ * @param endpoints RocketMQ 端点
|
|
|
+ * @return 客户端配置
|
|
|
*/
|
|
|
- @FunctionalInterface
|
|
|
- public interface TransactionChecker {
|
|
|
- TransactionResolution check(Message message);
|
|
|
+ private static ClientConfiguration defaultClientConfig(String endpoints) {
|
|
|
+ return ClientConfiguration.newBuilder()
|
|
|
+ .setEndpoints(endpoints)
|
|
|
+ .setRequestTimeout(java.time.Duration.ofSeconds(3))
|
|
|
+ .build();
|
|
|
}
|
|
|
}
|