19 KiB
19 KiB
Prompt - Metadata API客户端封装
输入引用
引用相关的 docs 文档链接:
- REQ-010-5.md - Metadata API客户端封装需求文档
- REQ-010-1.md - 数据库表结构设计和创建
- REQ-010-2.md - 基础实体类和Mapper创建
- REQ-010-3.md - Salesforce组织配置管理
- REQ-010-4.md - 元数据任务定义管理
- 0014-metadata-api-client.md - Metadata API客户端封装架构决策
Context Maps
强制列出本次 Prompt 依赖的 Canvas 文件:
- Authentication.canvas - 项目架构视觉化展示
- 相关节点: 集成核心 - 提供与Salesforce的各种连接方式
- 相关节点: SessionManager - 会话管理,提供登录服务
目标
实现 Metadata API 客户端封装,提供 retrieve() 和 deploy() 方法的调用接口,支持异步执行、状态轮询、Job ID 获取、Zip 文件下载、部署结果解析、错误处理机制等。
输出格式
代码示例(语言:Java)
1. MetadataApiClient 客户端类
package com.datai.salesforce.metadata.client;
import com.datai.salesforce.auth.service.ISessionManager;
import com.datai.salesforce.metadata.exception.MetadataApiException;
import com.datai.salesforce.metadata.model.RetrieveRequest;
import com.datai.salesforce.metadata.model.RetrieveResult;
import com.datai.salesforce.metadata.model.DeployRequest;
import com.datai.salesforce.metadata.model.DeployResult;
import com.sforce.soap.metadata.*;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.ConnectorConfig;
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.util.zip.ZipInputStream;
/**
* Metadata API 客户端
*/
@Slf4j
@Component
public class MetadataApiClient {
@Autowired
private ISessionManager sessionManager;
/**
* 创建 Metadata 连接
*/
private MetadataConnection createMetadataConnection(Long orgConfigId) throws ConnectionException {
String accessToken = sessionManager.getAccessToken(orgConfigId);
String instanceUrl = sessionManager.getInstanceUrl(orgConfigId);
ConnectorConfig config = new ConnectorConfig();
config.setSessionId(accessToken);
config.setRestEndpoint(instanceUrl + "/services/Soap/m/" + getApiVersion());
config.setCompression(true);
config.setTraceMessage(false);
return new MetadataConnection(config);
}
/**
* 获取 API 版本
*/
private String getApiVersion() {
return "60.0";
}
/**
* 执行 retrieve 操作
*/
public RetrieveResult retrieve(RetrieveRequest request) throws MetadataApiException {
try {
MetadataConnection connection = createMetadataConnection(request.getOrgConfigId());
com.sforce.soap.metadata.RetrieveRequest sfRequest = new com.sforce.soap.metadata.RetrieveRequest();
sfRequest.setApiVersion(getApiVersion());
sfRequest.setPackageNames(request.getPackageNames());
sfRequest.setSinglePackage(request.isSinglePackage());
sfRequest.setSpecificFiles(request.getSpecificFiles());
sfRequest.setUnpackaged(request.getUnpackaged());
AsyncResult asyncResult = connection.retrieve(sfRequest);
return pollRetrieveStatus(connection, asyncResult.getId(), request.getTimeout());
} catch (ConnectionException e) {
log.error("Retrieve operation failed", e);
throw new MetadataApiException("Retrieve operation failed: " + e.getMessage(), e);
}
}
/**
* 轮询 retrieve 状态
*/
private RetrieveResult pollRetrieveStatus(MetadataConnection connection, String jobId, long timeout) throws ConnectionException, MetadataApiException {
long startTime = System.currentTimeMillis();
while (true) {
AsyncResult result = connection.checkStatus(new String[]{jobId})[0];
if (result.isDone()) {
RetrieveResult retrieveResult = connection.checkRetrieveStatus(jobId);
return retrieveResult;
}
if (System.currentTimeMillis() - startTime > timeout) {
throw new MetadataApiException("Retrieve operation timed out");
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MetadataApiException("Retrieve operation interrupted", e);
}
}
}
/**
* 执行 deploy 操作
*/
public DeployResult deploy(DeployRequest request) throws MetadataApiException {
try {
MetadataConnection connection = createMetadataConnection(request.getOrgConfigId());
com.sforce.soap.metadata.DeployOptions options = new com.sforce.soap.metadata.DeployOptions();
options.setAllowMissingFiles(request.isAllowMissingFiles());
options.setAutoUpdatePackage(request.isAutoUpdatePackage());
options.setCheckOnly(request.isCheckOnly());
options.setIgnoreWarnings(request.isIgnoreWarnings());
options.setPerformRetrieve(request.isPerformRetrieve());
options.setPurgeOnDelete(request.isPurgeOnDelete());
options.setRollbackOnError(request.isRollbackOnError());
options.setTestLevel(request.getTestLevel());
options.setRunTests(request.getRunTests());
AsyncResult asyncResult = connection.deploy(request.getZipFile(), options);
return pollDeployStatus(connection, asyncResult.getId(), request.getTimeout());
} catch (ConnectionException e) {
log.error("Deploy operation failed", e);
throw new MetadataApiException("Deploy operation failed: " + e.getMessage(), e);
}
}
/**
* 轮询 deploy 状态
*/
private DeployResult pollDeployStatus(MetadataConnection connection, String jobId, long timeout) throws ConnectionException, MetadataApiException {
long startTime = System.currentTimeMillis();
while (true) {
AsyncResult result = connection.checkStatus(new String[]{jobId})[0];
if (result.isDone()) {
DeployResult deployResult = connection.checkDeployStatus(jobId, true);
return deployResult;
}
if (System.currentTimeMillis() - startTime > timeout) {
throw new MetadataApiException("Deploy operation timed out");
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MetadataApiException("Deploy operation interrupted", e);
}
}
}
/**
* 下载 Zip 文件
*/
public byte[] downloadZipFile(RetrieveResult result) throws MetadataApiException {
try {
return result.getZipFile();
} catch (Exception e) {
log.error("Download zip file failed", e);
throw new MetadataApiException("Download zip file failed: " + e.getMessage(), e);
}
}
/**
* 解压 Zip 文件
*/
public void unzipFile(byte[] zipFile, String targetDir) throws MetadataApiException {
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipFile))) {
java.util.zip.ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
java.io.File file = new java.io.File(targetDir, entry.getName());
if (entry.isDirectory()) {
file.mkdirs();
} else {
file.getParentFile().mkdirs();
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(file)) {
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}
}
zis.closeEntry();
}
} catch (IOException e) {
log.error("Unzip file failed", e);
throw new MetadataApiException("Unzip file failed: " + e.getMessage(), e);
}
}
}
2. RetrieveRequest 类
package com.datai.salesforce.metadata.model;
import lombok.Data;
/**
* Retrieve 请求
*/
@Data
public class RetrieveRequest {
private Long orgConfigId;
private String[] packageNames;
private boolean singlePackage;
private String[] specificFiles;
private com.sforce.soap.metadata.Package unpackaged;
private long timeout = 300000; // 5 minutes
}
3. RetrieveResult 类
package com.datai.salesforce.metadata.model;
import lombok.Data;
/**
* Retrieve 结果
*/
@Data
public class RetrieveResult {
private String id;
private boolean success;
private String status;
private String message;
private byte[] zipFile;
private String[] fileProperties;
}
4. DeployRequest 类
package com.datai.salesforce.metadata.model;
import lombok.Data;
/**
* Deploy 请求
*/
@Data
public class DeployRequest {
private Long orgConfigId;
private byte[] zipFile;
private boolean allowMissingFiles;
private boolean autoUpdatePackage;
private boolean checkOnly;
private boolean ignoreWarnings;
private boolean performRetrieve;
private boolean purgeOnDelete;
private boolean rollbackOnError;
private String testLevel;
private String[] runTests;
private long timeout = 300000; // 5 minutes
}
5. DeployResult 类
package com.datai.salesforce.metadata.model;
import lombok.Data;
import java.util.List;
/**
* Deploy 结果
*/
@Data
public class DeployResult {
private String id;
private boolean success;
private String status;
private String message;
private String startDate;
private String finishDate;
private int numberComponentErrors;
private int numberComponentsDeployed;
private int numberComponentsTotal;
private int numberTestErrors;
private int numberTestsCompleted;
private int numberTestsTotal;
private String testCoverage;
private List<DeployMessage> messages;
}
6. DeployMessage 类
package com.datai.salesforce.metadata.model;
import lombok.Data;
/**
* Deploy 消息
*/
@Data
public class DeployMessage {
private String fileName;
private String componentName;
private String problem;
private String line;
private String column;
private boolean success;
}
7. MetadataApiException 类
package com.datai.salesforce.metadata.exception;
/**
* Metadata API 异常
*/
public class MetadataApiException extends RuntimeException {
public MetadataApiException(String message) {
super(message);
}
public MetadataApiException(String message, Throwable cause) {
super(message, cause);
}
}
8. 异步配置类
package com.datai.salesforce.metadata.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* 异步配置
*/
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "metadataTaskExecutor")
public Executor metadataTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("metadata-async-");
executor.initialize();
return executor;
}
}
9. 异步服务接口
package com.datai.salesforce.metadata.service;
import com.datai.salesforce.metadata.model.DeployRequest;
import com.datai.salesforce.metadata.model.DeployResult;
import com.datai.salesforce.metadata.model.RetrieveRequest;
import com.datai.salesforce.metadata.model.RetrieveResult;
import java.util.concurrent.CompletableFuture;
/**
* Metadata API 异步服务
*/
public interface IMetadataApiAsyncService {
/**
* 异步执行 retrieve 操作
*/
CompletableFuture<RetrieveResult> retrieveAsync(RetrieveRequest request);
/**
* 异步执行 deploy 操作
*/
CompletableFuture<DeployResult> deployAsync(DeployRequest request);
}
10. 异步服务实现
package com.datai.salesforce.metadata.service.impl;
import com.datai.salesforce.metadata.client.MetadataApiClient;
import com.datai.salesforce.metadata.exception.MetadataApiException;
import com.datai.salesforce.metadata.model.DeployRequest;
import com.datai.salesforce.metadata.model.DeployResult;
import com.datai.salesforce.metadata.model.RetrieveRequest;
import com.datai.salesforce.metadata.model.RetrieveResult;
import com.datai.salesforce.metadata.service.IMetadataApiAsyncService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
/**
* Metadata API 异步服务实现
*/
@Slf4j
@Service
public class MetadataApiAsyncServiceImpl implements IMetadataApiAsyncService {
@Autowired
private MetadataApiClient metadataApiClient;
@Async("metadataTaskExecutor")
@Override
public CompletableFuture<RetrieveResult> retrieveAsync(RetrieveRequest request) {
try {
RetrieveResult result = metadataApiClient.retrieve(request);
return CompletableFuture.completedFuture(result);
} catch (MetadataApiException e) {
log.error("Async retrieve failed", e);
return CompletableFuture.failedFuture(e);
}
}
@Async("metadataTaskExecutor")
@Override
public CompletableFuture<DeployResult> deployAsync(DeployRequest request) {
try {
DeployResult result = metadataApiClient.deploy(request);
return CompletableFuture.completedFuture(result);
} catch (MetadataApiException e) {
log.error("Async deploy failed", e);
return CompletableFuture.failedFuture(e);
}
}
}
11. 单元测试
package com.datai.salesforce.metadata.client;
import com.datai.salesforce.metadata.exception.MetadataApiException;
import com.datai.salesforce.metadata.model.DeployRequest;
import com.datai.salesforce.metadata.model.DeployResult;
import com.datai.salesforce.metadata.model.RetrieveRequest;
import com.datai.salesforce.metadata.model.RetrieveResult;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MetadataApiClientTest {
@Mock
private ISessionManager sessionManager;
@InjectMocks
private MetadataApiClient metadataApiClient;
@Test
void testRetrieve() throws MetadataApiException {
RetrieveRequest request = new RetrieveRequest();
request.setOrgConfigId(1L);
request.setTimeout(30000);
when(sessionManager.getAccessToken(any())).thenReturn("test-token");
when(sessionManager.getInstanceUrl(any())).thenReturn("https://test.salesforce.com");
RetrieveResult result = metadataApiClient.retrieve(request);
assertNotNull(result);
}
@Test
void testDeploy() throws MetadataApiException {
DeployRequest request = new DeployRequest();
request.setOrgConfigId(1L);
request.setZipFile(new byte[0]);
request.setTimeout(30000);
when(sessionManager.getAccessToken(any())).thenReturn("test-token");
when(sessionManager.getInstanceUrl(any())).thenReturn("https://test.salesforce.com");
DeployResult result = metadataApiClient.deploy(request);
assertNotNull(result);
}
}
约束
列出使用此提示词时的约束条件,例如:
- 技术栈限制: 必须使用 Salesforce WSC (Web Service Connector) 库
- 架构约束: 必须遵循 Authentication.canvas 中定义的架构和调用关系
- 模块约束: 必须在 datai-salesforce-metadata 模块下实现
- 认证约束: 必须使用 SessionManager 进行会话管理和自动重新登录
- API 约束: 必须遵循 Salesforce Metadata API 调用规范
- 异步约束: 必须使用异步线程池执行长时间任务
- 依赖约束: 必须依赖于 REQ-010-1, REQ-010-2, REQ-010-3, REQ-010-4
Rule Set
"请严格参考 @Authentication.canvas 中的状态机转移逻辑,不要自行发挥。"
具体规则:
- 必须使用 Canvas 中定义的类名和方法名
- 必须遵循 Canvas 中定义的调用关系
- 必须参考 Canvas 中的流程图逻辑
- 必须使用 SessionManager 进行会话管理和自动重新登录
- 必须使用现有的认证模块进行 OAuth 认证
- 必须使用现有的集成核心功能进行 API 调用
- 必须遵循现有的异常处理机制
- 必须遵循现有的日志记录规范
验收标准
定义验证输出质量的具体标准,例如:
- 功能完整性: 所有 API 方法能够正常工作,异步执行机制正常
- 代码规范性: 代码符合项目编码规范,有清晰的注释
- 性能要求: 异步执行不影响系统响应,轮询频率合理
- 可维护性: 代码结构清晰,易于扩展和维护
- 可测试性: 代码易于单元测试和集成测试
风险
识别使用此提示词可能带来的风险,例如:
- WSC 库版本兼容性风险: WSC 库版本可能与项目其他依赖不兼容
- 异步执行风险: 异步执行机制复杂可能导致状态管理困难
- 状态轮询风险: 状态轮询频率不当可能导致 API 限流
- Zip 文件处理风险: Zip 文件处理不当可能导致内存溢出
- 错误处理风险: 错误处理不完善可能导致任务失败无法恢复
使用记录
| 日期 | 使用场景 | 输入参数 | 输出结果 | 反馈 | 改进措施 |
|---|---|---|---|---|---|
| 2026-01-19 | Metadata API客户端封装实现 | REQ-010-5 需求文档、ADR 文档 | MetadataApiClient 客户端类、retrieve() 方法、deploy() 方法、状态轮询机制、Zip 文件处理、部署结果解析、错误处理机制 | 待反馈 | 待改进 |