1、轮训结果
// Result:API的返回结果,
// Status:QUEUED、IN_PROGRESS、COMPLETED、FAILED
// TaskId:TaskId
// UsageMap:UasgeMap
{
"RequestId": "01A11A0C-40B0-1181-B848-7FF74C3DB014",
"Message": "Success",
"Data": {
"Status": "COMPLETED",
"TaskId": "TID_3d32692c-ae96-438b-b1cd-3d5046e8280e",
"Result": "API返回实际出参结构"
},
"Code": "success",
"Success": true
}2、ROCKETMQ订阅
1. 接入流程
向服务提供方申请生产环境的接入点、ConsumerGroup、消费凭据,以及所属 UID 的解密密钥。
配置
uid-apiTypeTag 订阅,启动消费者。收到消息后解析加密信封,使用用户密钥进行 AES-256-GCM 解密。
根据
Data.TaskId幂等处理,根据Data.Status判断任务结果。业务数据持久化成功后确认消费;处理异常时返回消费失败。
接入前注意:
MQ 登录凭据和消息解密密钥是两套独立凭据,不能混用。
客户端只获取自己的用户密钥,不需要、也不应获取服务端主密钥或复制服务端密钥派生工具。
当前结果通知可能重复,也可能因发送失败而缺失。结果查询接口应作为兜底,不能把“没有收到通知”判断为任务失败。
2. 订阅规则
消息元数据:
字段 | 当前约定 |
Topic | 服务方交付的结果 Topic |
Key |
|
Tag |
|
Body | UTF-8 编码的加密信封 JSON |
示例 UID 为 1234567890,示例 API code 为 api-a:
1234567890-api-a
订阅同一 UID 的多个 API:
1234567890-api-a||1234567890-api-b
一条消息只有一个 Tag;消费表达式中的 || 表示“或”。Tag 精确匹配且区分大小写,1234567890-* 不表示 UID 前缀匹配;不要使用 * 接收整个共享 Topic。同一 ConsumerGroup 内实例的订阅关系必须保持一致,不同订阅需求使用不同 Group。消息过滤
3. 消息格式与加密协议
3.1 收到的消息体:加密信封
{
"Version": 2,
"Algorithm": "AES-256-GCM",
"Iv": "<Base64 编码的 12 字节 IV>",
"Ciphertext": "<Base64 编码的密文及认证标签>"
}
3.2 解密后的业务结果
{
"RequestId": null,
"Code": "success",
"Success": true,
"Message": "Success",
"Data": {
"Status": "COMPLETED",
"Result": "{\"text\":\"翻译结果\"}",
"TaskId": "task-001",
"UsageMap": {}
}
}
失败结果示意:
{
"Status": "FAILED",
"TaskId": "task-001",
"Result": "{\"errorCode\":\"<实际错误码>\",\"errorMessage\":\"<失败原因>\"}",
"UsageMap": null
}
4.接入方案
4.1 POM依赖
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client-java</artifactId>
<version>5.0.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.30</version>
</dependency>
</dependencies>
4.2 Java 解密工具(可独立使用)
keySecret 向服务提供方询问获取
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.Base64;
public final class AsyncTaskResultDecryptor {
private AsyncTaskResultDecryptor() {}
public static JSONObject decryptMessage(byte[] body, String keySecret)
throws GeneralSecurityException {
if (body == null || body.length == 0
|| keySecret == null || keySecret.isBlank()) {
throw new IllegalArgumentException("body and keySecret are required");
}
JSONObject envelope = JSON.parseObject(new String(body, StandardCharsets.UTF_8));
if (envelope == null || !Integer.valueOf(2).equals(envelope.get("Version"))
|| !"AES-256-GCM".equals(envelope.get("Algorithm"))) {
throw new IllegalArgumentException("Unsupported encryption protocol");
}
String ivBase64 = envelope.getString("Iv");
String ciphertextBase64 = envelope.getString("Ciphertext");
if (ivBase64 == null || ivBase64.isBlank()
|| ciphertextBase64 == null || ciphertextBase64.isBlank()) {
throw new IllegalArgumentException("Iv and Ciphertext are required");
}
byte[] key = Base64.getDecoder().decode(keySecret);
byte[] iv = Base64.getDecoder().decode(ivBase64);
byte[] ciphertext = Base64.getDecoder().decode(ciphertextBase64);
if (key.length != 32 || iv.length != 12 || ciphertext.length < 16) {
throw new IllegalArgumentException("Invalid key, IV or ciphertext length");
}
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
new GCMParameterSpec(128, iv));
byte[] plaintext = cipher.doFinal(ciphertext);
JSONObject result = JSON.parseObject(new String(plaintext, StandardCharsets.UTF_8));
if (result == null) {
throw new IllegalArgumentException("Result JSON is required");
}
return result;
}
}
4.3. Java 消费者接入示例
4.3.1 配置
以下是配置项说明,不要把真实密码或密钥提交到仓库。示例从环境变量读取,生产环境可改为密钥管理系统或受控配置注入。
ASYNC_MQ_ENDPOINTS=rmq-cn-x684z19du01.cn-beijing.rmq.aliyuncs.com:8080
ASYNC_MQ_NAMESPACE=rmq-cn-x684z19du01
ASYNC_MQ_TOPIC=ASYNC_TASK_RESULT_TOPIC
ASYNC_MQ_GROUP=CID_ASYNC_TASK_RESULT_{UID}
ASYNC_MQ_USERNAME=<消费账号:询问服务提供方>
ASYNC_MQ_PASSWORD=<消费密码:询问服务提供方>
ASYNC_MQ_UID=1234567890
ASYNC_MQ_API_TYPES=api-a,api-b
ASYNC_RESULT_KEY_SECRET=<该UID的用户密钥:询问服务提供方>
4.3.2 消费、解密与确认
import com.alibaba.fastjson.JSONObject;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.StaticSessionCredentialsProvider;
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 java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
public final class AsyncTaskResultConsumer {
private static final Logger LOG = Logger.getLogger(AsyncTaskResultConsumer.class.getName());
@FunctionalInterface
public interface ResultHandler {
// 必须在业务数据可靠持久化或幂等确认已处理后才正常返回;失败抛异常。
void handle(String taskId, String tag, JSONObject result) throws Exception;
}
public static PushConsumer start(ResultHandler handler) throws Exception {
Objects.requireNonNull(handler, "handler");
String uid = required("ASYNC_MQ_UID");
Set<String> tags = Arrays.stream(required("ASYNC_MQ_API_TYPES").split(",", -1))
.map(String::trim)
.map(apiType -> {
if (apiType.isEmpty() || apiType.contains("|") || apiType.contains("*")) {
throw new IllegalArgumentException("Invalid apiType");
}
return uid + "-" + apiType;
})
.collect(Collectors.toSet());
String keySecret = required("ASYNC_RESULT_KEY_SECRET");
if (Base64.getDecoder().decode(keySecret).length != 32) {
throw new IllegalArgumentException("Expected a 32-byte AES key");
}
var config = ClientConfiguration.newBuilder()
.setEndpoints(required("ASYNC_MQ_ENDPOINTS"))
.setCredentialProvider(new StaticSessionCredentialsProvider(
required("ASYNC_MQ_USERNAME"), required("ASYNC_MQ_PASSWORD")));
String namespace = System.getenv("ASYNC_MQ_NAMESPACE");
if (namespace != null && !namespace.isBlank()) {
config.setNamespace(namespace);
}
FilterExpression filter = new FilterExpression(
tags.stream().sorted().collect(Collectors.joining("||")),
FilterExpressionType.TAG);
return ClientServiceProvider.loadService().newPushConsumerBuilder()
.setClientConfiguration(config.build())
.setConsumerGroup(required("ASYNC_MQ_GROUP"))
.setSubscriptionExpressions(Map.of(required("ASYNC_MQ_TOPIC"), filter))
.setConsumptionThreadCount(8)
.setMessageListener(message -> {
try {
String tag = message.getTag().orElseThrow();
if (!tags.contains(tag)) {
throw new IllegalArgumentException("Unexpected message tag");
}
ByteBuffer buffer = message.getBody().duplicate();
byte[] body = new byte[buffer.remaining()];
buffer.get(body); // 不假定 ByteBuffer 一定有可访问的 array。
JSONObject result = AsyncTaskResultDecryptor.decryptMessage(body, keySecret);
JSONObject data = result.getJSONObject("Data");
if (data == null || data.getString("TaskId") == null
|| data.getString("TaskId").isBlank()) {
throw new IllegalArgumentException("Data.TaskId is required");
}
String status = data.getString("Status");
if (status == null || !Set.of("COMPLETED", "FAILED", "CANCELLED").contains(status)) {
throw new IllegalArgumentException("Unexpected task status");
}
handler.handle(data.getString("TaskId"), tag, result);
return ConsumeResult.SUCCESS;
} catch (Exception e) {
// 不记录凭据、消息体、明文或异常中的业务内容。
LOG.warning("Async result consumption failed, messageId="
+ message.getMessageId() + ", errorType=" + e.getClass().getSimpleName());
return ConsumeResult.FAILURE;
}
})
.build();
}
private static String required(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Missing environment variable: " + name);
}
return value;
}
}
该文章对您有帮助吗?