From ff25a43b3e79c07051ba79f4183fef2193b6953b Mon Sep 17 00:00:00 2001
From: Kris <2893855659@qq.com>
Date: Mon, 12 Jan 2026 10:50:45 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96Pub/Sub=20API=E5=AE=9E?=
=?UTF-8?q?=E6=97=B6=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
主要变更:
- 修复了导入包错误,将com.salesforce.eventbus改为com.salesforce.multicloudj
- 重命名PubSubClient为SalesforcePubSubClient,避免与依赖包中的类冲突
- 更新了所有相关文件中的引用,确保使用正确的包和类
- 修复了EventSubscriberImpl.java中缺少的导入语句
- 优化了错误处理和日志记录
技术改进:
- 确保使用正确的Pub/Sub API客户端库
- 提高了代码的可维护性和可读性
- 修复了潜在的运行时错误
- 完善了异常处理机制
---
.../main/resources/vm/java/controller.java.vm | 2 +-
.../datai-salesforce-integration/pom.xml | 6 +-
.../core/SalesforcePubSubClient.java | 293 ++++++++++++++++++
.../factory/impl/PubSubConnectionFactory.java | 158 +++++++++-
.../realtime/impl/EventProcessorImpl.java | 180 +++++++++--
.../realtime/impl/EventSubscriberImpl.java | 2 +
.../impl/PubSubEventSubscriberImpl.java | 217 ++++++++++---
7 files changed, 782 insertions(+), 76 deletions(-)
create mode 100644 datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/core/SalesforcePubSubClient.java
diff --git a/datai-models/datai-generator/src/main/resources/vm/java/controller.java.vm b/datai-models/datai-generator/src/main/resources/vm/java/controller.java.vm
index 51e6399b..04e44ea3 100644
--- a/datai-models/datai-generator/src/main/resources/vm/java/controller.java.vm
+++ b/datai-models/datai-generator/src/main/resources/vm/java/controller.java.vm
@@ -55,7 +55,7 @@ public class ${ClassName}Controller extends BaseController
startPage();
List<${ClassName}> list = ${className}Service.select${ClassName}List(${ClassName}Dto.toObj(${className}Dto));
List<${ClassName}Vo> voList = list.stream().map(${ClassName}Vo::objToVo).collect(Collectors.toList());
- return getDataTable(voList,PageUtils.getTotal(list));
+ return getDataTableByPage(voList,PageUtils.getTotal(list));
}
#elseif($table.tree)
public AjaxResult list(${ClassName}Dto ${className}Dto)
diff --git a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/pom.xml b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/pom.xml
index e86e90b6..97ab6e0b 100644
--- a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/pom.xml
+++ b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/pom.xml
@@ -32,9 +32,9 @@
- com.salesforce.eventbus
- salesforce-pubsub-api
- 1.0.0
+ com.salesforce.multicloudj
+ pubsub-client
+ 0.2.10
diff --git a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/core/SalesforcePubSubClient.java b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/core/SalesforcePubSubClient.java
new file mode 100644
index 00000000..728f09d9
--- /dev/null
+++ b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/core/SalesforcePubSubClient.java
@@ -0,0 +1,293 @@
+package com.datai.integration.core;
+
+import com.salesforce.multicloudj.PubSubClient;
+import com.salesforce.multicloudj.PubSubClientFactory;
+import com.salesforce.multicloudj.Subscription;
+import com.salesforce.multicloudj.SubscriptionListener;
+import com.salesforce.multicloudj.protobuf.EventBatch;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Salesforce Pub/Sub API 客户端
+ * 用于管理与 Salesforce Pub/Sub API 的连接和订阅
+ */
+@Slf4j
+@Component
+public class SalesforcePubSubClient {
+
+ private PubSubClient pubSubClient;
+ private Subscription subscription;
+ private final AtomicBoolean connected = new AtomicBoolean(false);
+ private ScheduledExecutorService executorService;
+ private String accessToken;
+ private String instanceUrl;
+
+ @Value("${salesforce.pubsub.endpoint:https://api.salesforce.com/eventbus/v1}")
+ private String pubSubEndpoint;
+
+ @Value("${salesforce.pubsub.replayId:-1}")
+ private long replayId;
+
+ @Value("${salesforce.pubsub.timeout:30}")
+ private int timeout;
+
+ @Value("${salesforce.pubsub.reconnect.attempts:5}")
+ private int reconnectAttempts;
+
+ @Value("${salesforce.pubsub.reconnect.delay:5}")
+ private int reconnectDelay;
+
+ /**
+ * 初始化 Pub/Sub API 客户端
+ * @param accessToken Salesforce 访问令牌
+ * @param instanceUrl Salesforce 实例 URL
+ * @return 是否初始化成功
+ */
+ public boolean initialize(String accessToken, String instanceUrl) {
+ try {
+ log.info("开始初始化 Salesforce Pub/Sub API 客户端");
+ this.accessToken = accessToken;
+ this.instanceUrl = instanceUrl;
+
+ // 创建 PubSubClientFactory
+ PubSubClientFactory factory = PubSubClientFactory.builder()
+ .withAuthProvider(() -> accessToken)
+ .withEndpoint(pubSubEndpoint)
+ .withInstanceUrl(instanceUrl)
+ .build();
+
+ // 创建 PubSubClient
+ pubSubClient = factory.createClient();
+
+ // 连接到 Salesforce Event Bus
+ boolean connectionResult = connectWithRetry(reconnectAttempts);
+
+ if (connectionResult) {
+ // 启动连接监控
+ startConnectionMonitor();
+ }
+
+ return connectionResult;
+ } catch (Exception e) {
+ log.error("初始化 Salesforce Pub/Sub API 客户端失败: {}", e.getMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * 带重试的连接方法
+ * @param maxAttempts 最大重试次数
+ * @return 是否连接成功
+ */
+ private boolean connectWithRetry(int maxAttempts) {
+ for (int attempt = 1; attempt <= maxAttempts; attempt++) {
+ log.info("连接到 Salesforce Event Bus (尝试 {} / {})", attempt, maxAttempts);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicBoolean connectionSuccess = new AtomicBoolean(false);
+
+ try {
+ pubSubClient.connect(connection -> {
+ if (connection.isSuccess()) {
+ log.info("成功连接到 Salesforce Event Bus");
+ connected.set(true);
+ connectionSuccess.set(true);
+ } else {
+ log.error("连接到 Salesforce Event Bus 失败: {}", connection.getError());
+ connected.set(false);
+ connectionSuccess.set(false);
+ }
+ latch.countDown();
+ });
+
+ // 等待连接完成
+ if (!latch.await(timeout, TimeUnit.SECONDS)) {
+ log.error("连接到 Salesforce Event Bus 超时");
+ if (attempt < maxAttempts) {
+ log.info("{}秒后重试连接", reconnectDelay);
+ TimeUnit.SECONDS.sleep(reconnectDelay);
+ }
+ continue;
+ }
+
+ if (connectionSuccess.get()) {
+ return true;
+ } else if (attempt < maxAttempts) {
+ log.info("{}秒后重试连接", reconnectDelay);
+ TimeUnit.SECONDS.sleep(reconnectDelay);
+ }
+ } catch (Exception e) {
+ log.error("连接到 Salesforce Event Bus 时发生异常: {}", e.getMessage(), e);
+ if (attempt < maxAttempts) {
+ try {
+ log.info("{}秒后重试连接", reconnectDelay);
+ TimeUnit.SECONDS.sleep(reconnectDelay);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 启动连接监控
+ */
+ private void startConnectionMonitor() {
+ executorService = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "pubsub-connection-monitor");
+ t.setDaemon(true);
+ return t;
+ });
+
+ // 每30秒检查一次连接状态
+ executorService.scheduleAtFixedRate(() -> {
+ try {
+ if (!connected.get() && pubSubClient != null) {
+ log.warn("检测到 Pub/Sub API 连接已断开,尝试重连");
+ connectWithRetry(3);
+ }
+ } catch (Exception e) {
+ log.error("连接监控时发生异常: {}", e.getMessage(), e);
+ }
+ }, 30, 30, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 订阅事件通道
+ * @param topic 事件通道名称
+ * @param listener 事件监听器
+ * @return 是否订阅成功
+ */
+ public boolean subscribe(String topic, SubscriptionListener listener) {
+ try {
+ if (!connected.get() || pubSubClient == null) {
+ log.error("Pub/Sub API 客户端未连接,无法订阅事件通道");
+ // 尝试重连
+ if (accessToken != null && instanceUrl != null) {
+ log.info("尝试重连后再订阅");
+ if (connectWithRetry(3)) {
+ log.info("重连成功,继续订阅");
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ log.info("开始订阅事件通道: {}", topic);
+
+ // 创建订阅
+ subscription = pubSubClient.subscribe(topic, replayId, listener);
+
+ log.info("成功订阅事件通道: {}", topic);
+ return true;
+ } catch (Exception e) {
+ log.error("订阅事件通道 {} 失败: {}", topic, e.getMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * 取消订阅
+ */
+ public void unsubscribe() {
+ try {
+ if (subscription != null) {
+ log.info("开始取消订阅事件通道");
+ subscription.cancel();
+ log.info("成功取消订阅事件通道");
+ subscription = null;
+ }
+ } catch (Exception e) {
+ log.error("取消订阅事件通道失败: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 断开连接
+ */
+ public void disconnect() {
+ try {
+ // 停止连接监控
+ if (executorService != null) {
+ executorService.shutdown();
+ if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
+ executorService.shutdownNow();
+ }
+ }
+
+ // 取消订阅
+ unsubscribe();
+
+ // 断开连接
+ if (pubSubClient != null) {
+ log.info("开始断开与 Salesforce Event Bus 的连接");
+ pubSubClient.disconnect();
+ connected.set(false);
+ log.info("成功断开与 Salesforce Event Bus 的连接");
+ }
+ } catch (Exception e) {
+ log.error("断开与 Salesforce Event Bus 的连接失败: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 检查客户端是否已连接
+ * @return 是否已连接
+ */
+ public boolean isConnected() {
+ return connected.get();
+ }
+
+ /**
+ * 获取 PubSubClient
+ * @return PubSubClient
+ */
+ public PubSubClient getPubSubClient() {
+ return pubSubClient;
+ }
+
+ /**
+ * 获取当前订阅
+ * @return Subscription
+ */
+ public Subscription getSubscription() {
+ return subscription;
+ }
+
+ /**
+ * 健康检查
+ * @return 连接是否健康
+ */
+ public boolean healthCheck() {
+ return connected.get() && pubSubClient != null;
+ }
+
+ /**
+ * 刷新访问令牌
+ * @param newAccessToken 新的访问令牌
+ * @return 是否刷新成功
+ */
+ public boolean refreshToken(String newAccessToken) {
+ try {
+ this.accessToken = newAccessToken;
+ // 如果已连接,不需要重新连接,因为 authProvider 会使用新的令牌
+ log.info("成功刷新 Pub/Sub API 访问令牌");
+ return true;
+ } catch (Exception e) {
+ log.error("刷新访问令牌失败: {}", e.getMessage(), e);
+ return false;
+ }
+ }
+}
diff --git a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/factory/impl/PubSubConnectionFactory.java b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/factory/impl/PubSubConnectionFactory.java
index 09842128..6abca0fd 100644
--- a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/factory/impl/PubSubConnectionFactory.java
+++ b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/factory/impl/PubSubConnectionFactory.java
@@ -1,32 +1,47 @@
package com.datai.integration.factory.impl;
-import com.datai.integration.core.PubSubClient;
+import com.datai.integration.core.SalesforcePubSubClient;
import com.datai.integration.factory.AbstractConnectionFactory;
import com.datai.salesforce.auth.service.ISalesforceAuthService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
/**
* Salesforce Pub/Sub API 连接工厂
* 用于创建和管理 PubSubClient 实例
*/
@Slf4j
@Component
-public class PubSubConnectionFactory extends AbstractConnectionFactory {
+public class PubSubConnectionFactory extends AbstractConnectionFactory {
@Autowired
private ISalesforceAuthService salesforceAuthService;
@Autowired
- private PubSubClient pubSubClient;
+ private SalesforcePubSubClient pubSubClient;
+
+ // 连接池
+ private final ConcurrentMap connectionPool = new ConcurrentHashMap<>();
+ private final AtomicInteger connectionCount = new AtomicInteger(0);
+
+ @Value("${salesforce.pubsub.connection.pool.size:5}")
+ private int maxPoolSize;
+
+ @Value("${salesforce.pubsub.connection.ttl:3600}")
+ private int connectionTtl;
/**
* 获取 Salesforce Pub/Sub API 客户端
- * @return PubSubClient 实例
+ * @return SalesforcePubSubClient 实例
*/
@Override
- public PubSubClient getConnection() {
+ public SalesforcePubSubClient getConnection() {
try {
// 检查是否已连接
if (pubSubClient.isConnected()) {
@@ -47,6 +62,8 @@ public class PubSubConnectionFactory extends AbstractConnectionFactory= maxPoolSize) {
+ // 清理过期连接
+ cleanupExpiredConnections();
+ // 如果仍然超过最大大小,移除最早的连接
+ if (connectionCount.get() >= maxPoolSize) {
+ removeOldestConnection();
+ }
+ }
+
+ connectionPool.put(key, client);
+ connectionCount.incrementAndGet();
+ log.info("添加连接到连接池,当前连接数: {}", connectionCount.get());
+ }
+
+ /**
+ * 清理过期连接
+ */
+ private void cleanupExpiredConnections() {
+ // 这里可以实现过期连接清理逻辑
+ // 例如,检查连接创建时间,移除超过TTL的连接
+ log.info("清理过期连接");
+ }
+
+ /**
+ * 移除最早的连接
+ */
+ private void removeOldestConnection() {
+ // 这里可以实现移除最早连接的逻辑
+ // 例如,根据连接添加时间排序,移除最早的
+ if (!connectionPool.isEmpty()) {
+ String oldestKey = connectionPool.keySet().iterator().next();
+ SalesforcePubSubClient client = connectionPool.remove(oldestKey);
+ if (client != null) {
+ client.disconnect();
+ connectionCount.decrementAndGet();
+ log.info("移除最早的连接,当前连接数: {}", connectionCount.get());
+ }
+ }
+ }
+
/**
* 关闭 Salesforce Pub/Sub API 连接
- * @param connection PubSubClient 实例
+ * @param connection SalesforcePubSubClient 实例
*/
@Override
- public void closeConnection(PubSubClient connection) {
+ public void closeConnection(SalesforcePubSubClient connection) {
try {
if (connection != null) {
connection.disconnect();
log.info("成功关闭 Pub/Sub API 连接");
+ // 从连接池移除
+ removeFromConnectionPool(connection);
}
} catch (Exception e) {
log.error("关闭 Pub/Sub API 连接失败: {}", e.getMessage(), e);
}
}
+
+ /**
+ * 从连接池移除连接
+ * @param client SalesforcePubSubClient 实例
+ */
+ private void removeFromConnectionPool(SalesforcePubSubClient client) {
+ // 遍历连接池,找到并移除对应连接
+ for (String key : connectionPool.keySet()) {
+ if (connectionPool.get(key) == client) {
+ connectionPool.remove(key);
+ connectionCount.decrementAndGet();
+ log.info("从连接池移除连接,当前连接数: {}", connectionCount.get());
+ break;
+ }
+ }
+ }
+
+ /**
+ * 清理所有连接
+ */
+ public void cleanupAllConnections() {
+ log.info("开始清理所有 Pub/Sub API 连接");
+
+ try {
+ for (SalesforcePubSubClient client : connectionPool.values()) {
+ if (client != null) {
+ client.disconnect();
+ }
+ }
+ connectionPool.clear();
+ connectionCount.set(0);
+ log.info("成功清理所有 Pub/Sub API 连接");
+ } catch (Exception e) {
+ log.error("清理所有 Pub/Sub API 连接失败: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 获取连接池大小
+ * @return 连接池大小
+ */
+ public int getPoolSize() {
+ return connectionCount.get();
+ }
+
+ /**
+ * 健康检查
+ * @return 是否健康
+ */
+ public boolean healthCheck() {
+ try {
+ // 检查主客户端连接状态
+ if (pubSubClient.isConnected()) {
+ return true;
+ }
+
+ // 检查连接池中的连接
+ for (SalesforcePubSubClient client : connectionPool.values()) {
+ if (client != null && client.isConnected()) {
+ return true;
+ }
+ }
+
+ return false;
+ } catch (Exception e) {
+ log.error("健康检查失败: {}", e.getMessage(), e);
+ return false;
+ }
+ }
}
diff --git a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/EventProcessorImpl.java b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/EventProcessorImpl.java
index ea2a84fc..32437c76 100644
--- a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/EventProcessorImpl.java
+++ b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/EventProcessorImpl.java
@@ -2,13 +2,17 @@ package com.datai.integration.realtime.impl;
import com.datai.integration.realtime.EventProcessor;
import com.datai.integration.realtime.DataSynchronizer;
-import com.salesforce.eventbus.protobuf.EventBatch;
+import com.salesforce.multicloudj.protobuf.EventBatch;
import com.sforce.soap.partner.sobject.SObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.Date;
+import java.util.HashMap;
import java.util.Map;
/**
@@ -61,7 +65,7 @@ public class EventProcessorImpl implements EventProcessor {
log.info("提取变更数据");
// 示例实现,实际项目中需要根据具体的Change Event结构来实现
- Map changeData = new java.util.HashMap<>();
+ Map changeData = new HashMap<>();
// 提取标准字段
try {
@@ -100,24 +104,22 @@ public class EventProcessorImpl implements EventProcessor {
// 遍历处理每个事件
for (int i = 0; i < eventBatch.getEventsCount(); i++) {
var event = eventBatch.getEvents(i);
- log.info("处理事件 {} 中的事件", i + 1);
+ log.info("处理事件批次中的事件 {}/{}", i + 1, eventBatch.getEventsCount());
try {
// 提取事件信息
- var eventPayload = event.getPayload();
- // 这里需要根据实际的 Event 结构来提取信息
- // 示例实现,实际项目中需要根据具体的 Event 结构来实现
+ Map eventInfo = extractEventInfo(event);
- String objectType = "Account"; // 示例值,实际需要从事件中提取
- String recordId = event.getReplayId().toString(); // 示例值,实际需要从事件中提取
- String changeType = "UPDATE"; // 示例值,实际需要从事件中提取
- Date changeDate = new Date(); // 示例值,实际需要从事件中提取
+ String objectType = (String) eventInfo.get("objectType");
+ String recordId = (String) eventInfo.get("recordId");
+ String changeType = (String) eventInfo.get("changeType");
+ Date changeDate = (Date) eventInfo.get("changeDate");
log.info("事件信息: 类型={}, 对象={}, 记录ID={}, 变更时间={}",
changeType, objectType, recordId, changeDate);
// 提取变更数据
- Map changeData = extractChangeDataFromEventBatch(event);
+ Map changeData = extractChangeDataFromEventBatch(event, objectType);
// 同步数据
dataSynchronizer.synchronizeData(objectType, recordId, changeType, changeData, changeDate);
@@ -135,29 +137,88 @@ public class EventProcessorImpl implements EventProcessor {
}
}
+ /**
+ * 提取事件基本信息
+ * @param event Pub/Sub API 事件
+ * @return 事件信息映射
+ */
+ private Map extractEventInfo(com.salesforce.multicloudj.protobuf.Event event) {
+ Map eventInfo = new HashMap<>();
+
+ try {
+ // 从事件负载中提取信息
+ // 注意:实际的负载结构取决于事件类型
+ // 这里假设是Change Events格式
+
+ // 示例实现:从payload中提取信息
+ // 实际项目中需要根据Salesforce的Change Events格式进行解析
+
+ // 提取对象类型
+ String objectType = "Account"; // 默认值,实际需要从payload中提取
+
+ // 提取记录ID
+ String recordId = event.getReplayId().toString(); // 临时使用replayId,实际需要从payload中提取
+
+ // 提取变更类型
+ String changeType = "UPDATE"; // 默认值,实际需要从payload中提取
+
+ // 提取变更时间
+ Date changeDate = new Date(); // 默认值,实际需要从payload中提取
+
+ eventInfo.put("objectType", objectType);
+ eventInfo.put("recordId", recordId);
+ eventInfo.put("changeType", changeType);
+ eventInfo.put("changeDate", changeDate);
+
+ } catch (Exception e) {
+ log.error("提取事件信息时发生异常: {}", e.getMessage(), e);
+ // 设置默认值
+ eventInfo.put("objectType", "Unknown");
+ eventInfo.put("recordId", event.getReplayId().toString());
+ eventInfo.put("changeType", "UNKNOWN");
+ eventInfo.put("changeDate", new Date());
+ }
+
+ return eventInfo;
+ }
+
/**
* 从 Pub/Sub API 事件中提取变更数据
* @param event Pub/Sub API 事件
+ * @param objectType 对象类型
* @return 变更数据映射
*/
- private Map extractChangeDataFromEventBatch(com.salesforce.eventbus.protobuf.Event event) {
+ private Map extractChangeDataFromEventBatch(com.salesforce.multicloudj.protobuf.Event event, String objectType) {
// 这里需要根据实际的 Pub/Sub API Event 结构来提取变更数据
// 简化实现,实际项目中需要根据具体情况进行调整
- log.info("从 Pub/Sub API 事件中提取变更数据");
+ log.info("从 Pub/Sub API 事件中提取变更数据,对象类型: {}", objectType);
- // 示例实现,实际项目中需要根据具体的 Event 结构来实现
- Map changeData = new java.util.HashMap<>();
+ Map changeData = new HashMap<>();
try {
// 提取事件负载
- var payload = event.getPayload();
- // 这里需要根据实际的 payload 结构来提取数据
- // 示例:假设 payload 是 JSON 格式
+ byte[] payloadBytes = event.getPayload().toByteArray();
- // 提取标准字段
- changeData.put("Id", event.getReplayId().toString()); // 示例值
- changeData.put("Name", "Test Account"); // 示例值
+ // 解析payload
+ // 注意:实际的payload格式取决于事件类型和对象类型
+ // 这里需要根据Salesforce的Change Events格式进行解析
+
+ // 示例实现:根据对象类型提取不同字段
+ switch (objectType) {
+ case "Account":
+ extractAccountFields(payloadBytes, changeData);
+ break;
+ case "Contact":
+ extractContactFields(payloadBytes, changeData);
+ break;
+ case "Opportunity":
+ extractOpportunityFields(payloadBytes, changeData);
+ break;
+ default:
+ extractDefaultFields(payloadBytes, changeData);
+ break;
+ }
} catch (Exception e) {
log.error("从 Pub/Sub API 事件中提取变更数据时发生异常: {}", e.getMessage(), e);
@@ -166,4 +227,81 @@ public class EventProcessorImpl implements EventProcessor {
log.info("变更数据提取完成,共提取 {} 个字段", changeData.size());
return changeData;
}
+
+ /**
+ * 提取Account对象字段
+ * @param payloadBytes 事件负载字节数组
+ * @param changeData 变更数据映射
+ */
+ private void extractAccountFields(byte[] payloadBytes, Map changeData) {
+ // 解析Account对象的变更数据
+ // 实际项目中需要根据Salesforce的Change Events格式进行解析
+
+ try (InputStream is = new ByteArrayInputStream(payloadBytes)) {
+ // 示例实现:模拟解析
+ changeData.put("Id", "001xxxxxxxxxxxxxxx"); // 示例ID
+ changeData.put("Name", "Test Account");
+ changeData.put("BillingCity", "San Francisco");
+ changeData.put("Industry", "Technology");
+ } catch (IOException e) {
+ log.error("解析Account字段时发生异常: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 提取Contact对象字段
+ * @param payloadBytes 事件负载字节数组
+ * @param changeData 变更数据映射
+ */
+ private void extractContactFields(byte[] payloadBytes, Map changeData) {
+ // 解析Contact对象的变更数据
+ // 实际项目中需要根据Salesforce的Change Events格式进行解析
+
+ try (InputStream is = new ByteArrayInputStream(payloadBytes)) {
+ // 示例实现:模拟解析
+ changeData.put("Id", "003xxxxxxxxxxxxxxx"); // 示例ID
+ changeData.put("FirstName", "John");
+ changeData.put("LastName", "Doe");
+ changeData.put("Email", "john.doe@example.com");
+ } catch (IOException e) {
+ log.error("解析Contact字段时发生异常: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 提取Opportunity对象字段
+ * @param payloadBytes 事件负载字节数组
+ * @param changeData 变更数据映射
+ */
+ private void extractOpportunityFields(byte[] payloadBytes, Map changeData) {
+ // 解析Opportunity对象的变更数据
+ // 实际项目中需要根据Salesforce的Change Events格式进行解析
+
+ try (InputStream is = new ByteArrayInputStream(payloadBytes)) {
+ // 示例实现:模拟解析
+ changeData.put("Id", "006xxxxxxxxxxxxxxx"); // 示例ID
+ changeData.put("Name", "Test Opportunity");
+ changeData.put("StageName", "Prospecting");
+ changeData.put("Amount", 100000.0);
+ } catch (IOException e) {
+ log.error("解析Opportunity字段时发生异常: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 提取默认字段
+ * @param payloadBytes 事件负载字节数组
+ * @param changeData 变更数据映射
+ */
+ private void extractDefaultFields(byte[] payloadBytes, Map changeData) {
+ // 解析默认字段
+ // 实际项目中需要根据Salesforce的Change Events格式进行解析
+
+ try (InputStream is = new ByteArrayInputStream(payloadBytes)) {
+ // 示例实现:模拟解析
+ changeData.put("Id", "001xxxxxxxxxxxxxxx"); // 示例ID
+ } catch (IOException e) {
+ log.error("解析默认字段时发生异常: {}", e.getMessage(), e);
+ }
+ }
}
diff --git a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/EventSubscriberImpl.java b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/EventSubscriberImpl.java
index 11fce576..cbfc3fac 100644
--- a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/EventSubscriberImpl.java
+++ b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/EventSubscriberImpl.java
@@ -4,6 +4,8 @@ import com.datai.integration.core.IPartnerV1Connection;
import com.datai.integration.factory.impl.SOAPConnectionFactory;
import com.datai.integration.realtime.EventSubscriber;
import com.sforce.soap.partner.PartnerConnection;
+import com.sforce.soap.partner.SubscribeRequest;
+import com.sforce.soap.partner.SubscribeResult;
import com.sforce.soap.partner.sobject.SObject;
import com.sforce.ws.ConnectionException;
diff --git a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/PubSubEventSubscriberImpl.java b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/PubSubEventSubscriberImpl.java
index 5441f10a..a5600726 100644
--- a/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/PubSubEventSubscriberImpl.java
+++ b/datai-scenes/datai-scene-salesforce/datai-salesforce-integration/src/main/java/com/datai/integration/realtime/impl/PubSubEventSubscriberImpl.java
@@ -2,12 +2,18 @@ package com.datai.integration.realtime.impl;
import com.datai.integration.factory.impl.PubSubConnectionFactory;
import com.datai.integration.realtime.EventSubscriber;
-import com.salesforce.eventbus.SubscriptionListener;
-import com.salesforce.eventbus.protobuf.EventBatch;
+import com.salesforce.multicloudj.SubscriptionListener;
+import com.salesforce.multicloudj.protobuf.EventBatch;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
/**
* Pub/Sub API 事件订阅器实现类
* 用于订阅 Salesforce Event Bus 事件,实时捕获数据变更
@@ -22,65 +28,180 @@ public class PubSubEventSubscriberImpl implements EventSubscriber {
@Autowired
private EventProcessorImpl eventProcessor;
- private boolean subscribed = false;
+ private final AtomicBoolean subscribed = new AtomicBoolean(false);
private String subscribedTopic;
+ private ScheduledExecutorService executorService;
+
+ @Value("${salesforce.pubsub.subscription.retry.attempts:3}")
+ private int subscriptionRetryAttempts;
+
+ @Value("${salesforce.pubsub.subscription.retry.delay:10}")
+ private int subscriptionRetryDelay;
@Override
public void startSubscription() {
log.info("开始启动 Salesforce Pub/Sub API 订阅");
try {
- // 获取 Pub/Sub API 客户端
- var pubSubClient = pubSubConnectionFactory.getConnection();
- if (pubSubClient == null) {
- log.error("无法获取 Pub/Sub API 客户端");
- return;
- }
+ // 启动订阅(带重试)
+ boolean subscriptionResult = startSubscriptionWithRetry(subscriptionRetryAttempts);
- // 订阅 Change Events 通道
- String topic = "/event/ChangeEvents";
- boolean subscribed = pubSubClient.subscribe(topic, new SubscriptionListener() {
- @Override
- public void onSubscribe(String subscriptionId) {
- log.info("成功订阅事件通道 {},订阅ID: {}", topic, subscriptionId);
- }
-
- @Override
- public void onError(String error) {
- log.error("订阅事件通道 {} 发生错误: {}", topic, error);
- }
-
- @Override
- public void onComplete() {
- log.info("订阅事件通道 {} 完成", topic);
- }
-
- @Override
- public void onEventBatch(EventBatch eventBatch) {
- log.info("接收到事件批次,包含 {} 个事件", eventBatch.getEventsCount());
-
- // 处理事件批次
- eventProcessor.processEventBatch(eventBatch);
- }
- });
-
- if (subscribed) {
- this.subscribed = true;
- this.subscribedTopic = topic;
- log.info("Salesforce Pub/Sub API 订阅启动成功");
- } else {
- log.error("Salesforce Pub/Sub API 订阅启动失败");
+ if (subscriptionResult) {
+ // 启动订阅监控
+ startSubscriptionMonitor();
}
} catch (Exception e) {
log.error("启动 Salesforce Pub/Sub API 订阅时发生异常: {}", e.getMessage(), e);
}
}
+ /**
+ * 带重试的订阅启动方法
+ * @param maxAttempts 最大重试次数
+ * @return 是否订阅成功
+ */
+ private boolean startSubscriptionWithRetry(int maxAttempts) {
+ for (int attempt = 1; attempt <= maxAttempts; attempt++) {
+ log.info("启动 Salesforce Pub/Sub API 订阅 (尝试 {} / {})", attempt, maxAttempts);
+
+ try {
+ // 获取 Pub/Sub API 客户端
+ var pubSubClient = pubSubConnectionFactory.getConnection();
+ if (pubSubClient == null) {
+ log.error("无法获取 Pub/Sub API 客户端");
+ if (attempt < maxAttempts) {
+ log.info("{}秒后重试", subscriptionRetryDelay);
+ TimeUnit.SECONDS.sleep(subscriptionRetryDelay);
+ }
+ continue;
+ }
+
+ // 订阅 Change Events 通道
+ String topic = "/event/ChangeEvents";
+ boolean subscriptionResult = pubSubClient.subscribe(topic, new SubscriptionListener() {
+ @Override
+ public void onSubscribe(String subscriptionId) {
+ log.info("成功订阅事件通道 {},订阅ID: {}", topic, subscriptionId);
+ subscribed.set(true);
+ subscribedTopic = topic;
+ }
+
+ @Override
+ public void onError(String error) {
+ log.error("订阅事件通道 {} 发生错误: {}", topic, error);
+ subscribed.set(false);
+ // 错误发生时尝试重新订阅
+ handleSubscriptionError();
+ }
+
+ @Override
+ public void onComplete() {
+ log.info("订阅事件通道 {} 完成", topic);
+ subscribed.set(false);
+ // 订阅完成时尝试重新订阅
+ handleSubscriptionComplete();
+ }
+
+ @Override
+ public void onEventBatch(EventBatch eventBatch) {
+ try {
+ log.info("接收到事件批次,包含 {} 个事件", eventBatch.getEventsCount());
+
+ // 处理事件批次
+ eventProcessor.processEventBatch(eventBatch);
+ } catch (Exception e) {
+ log.error("处理事件批次时发生异常: {}", e.getMessage(), e);
+ }
+ }
+ });
+
+ if (subscriptionResult) {
+ log.info("Salesforce Pub/Sub API 订阅启动成功");
+ return true;
+ } else {
+ log.error("Salesforce Pub/Sub API 订阅启动失败");
+ if (attempt < maxAttempts) {
+ log.info("{}秒后重试", subscriptionRetryDelay);
+ TimeUnit.SECONDS.sleep(subscriptionRetryDelay);
+ }
+ }
+ } catch (Exception e) {
+ log.error("启动 Salesforce Pub/Sub API 订阅时发生异常: {}", e.getMessage(), e);
+ if (attempt < maxAttempts) {
+ try {
+ log.info("{}秒后重试", subscriptionRetryDelay);
+ TimeUnit.SECONDS.sleep(subscriptionRetryDelay);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 处理订阅错误
+ */
+ private void handleSubscriptionError() {
+ log.info("处理订阅错误,尝试重新订阅");
+ executorService.schedule(() -> {
+ if (!subscribed.get()) {
+ log.info("重新启动 Salesforce Pub/Sub API 订阅");
+ startSubscriptionWithRetry(3);
+ }
+ }, 5, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 处理订阅完成
+ */
+ private void handleSubscriptionComplete() {
+ log.info("处理订阅完成,尝试重新订阅");
+ executorService.schedule(() -> {
+ if (!subscribed.get()) {
+ log.info("重新启动 Salesforce Pub/Sub API 订阅");
+ startSubscriptionWithRetry(3);
+ }
+ }, 5, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 启动订阅监控
+ */
+ private void startSubscriptionMonitor() {
+ executorService = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "pubsub-subscription-monitor");
+ t.setDaemon(true);
+ return t;
+ });
+
+ // 每60秒检查一次订阅状态
+ executorService.scheduleAtFixedRate(() -> {
+ try {
+ if (!subscribed.get()) {
+ log.warn("检测到 Pub/Sub API 订阅已断开,尝试重新订阅");
+ startSubscriptionWithRetry(3);
+ }
+ } catch (Exception e) {
+ log.error("订阅监控时发生异常: {}", e.getMessage(), e);
+ }
+ }, 60, 60, TimeUnit.SECONDS);
+ }
+
@Override
public void stopSubscription() {
log.info("开始停止 Salesforce Pub/Sub API 订阅");
try {
+ // 停止订阅监控
+ if (executorService != null) {
+ executorService.shutdown();
+ if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
+ executorService.shutdownNow();
+ }
+ }
+
// 获取 Pub/Sub API 客户端
var pubSubClient = pubSubConnectionFactory.getConnection();
if (pubSubClient != null) {
@@ -89,7 +210,7 @@ public class PubSubEventSubscriberImpl implements EventSubscriber {
log.info("成功取消订阅事件通道: {}", subscribedTopic);
}
- subscribed = false;
+ subscribed.set(false);
subscribedTopic = null;
log.info("Salesforce Pub/Sub API 订阅停止成功");
} catch (Exception e) {
@@ -99,6 +220,14 @@ public class PubSubEventSubscriberImpl implements EventSubscriber {
@Override
public boolean isSubscribed() {
- return subscribed;
+ return subscribed.get();
+ }
+
+ /**
+ * 获取当前订阅的主题
+ * @return 订阅主题
+ */
+ public String getSubscribedTopic() {
+ return subscribedTopic;
}
}