🎨 #3848 【企业微信】修复会话存档SDK生命周期管理导致的JVM崩溃问题

This commit is contained in:
Copilot
2026-01-16 10:29:47 +08:00
committed by GitHub
parent 30914f371d
commit 47b4431aa8
7 changed files with 807 additions and 0 deletions

View File

@@ -0,0 +1,295 @@
# 企业微信会话存档SDK安全使用指南
## 问题背景
在使用企业微信会话存档功能时部分开发者遇到了JVM崩溃的问题。典型错误信息如下
```
SIGSEGV (0xb) at pc=0x00007fcd50460d93
Problematic frame:
C [libWeWorkFinanceSdk_Java.so+0x260d93] WeWorkFinanceSdk::TryRefresh(std::string const&, std::string const&, int)+0x23
```
## 问题原因
旧版API设计存在以下问题
1. **SDK生命周期管理混乱**
- `getChatDatas()` 方法会返回SDK实例给调用方
- 开发者需要手动调用 `Finance.DestroySdk()` 来销毁SDK
- 但SDK在框架内部有7200秒的缓存机制
2. **手动销毁导致缓存失效**
- 当开发者手动销毁SDK后框架缓存中的SDK引用变为无效
- 后续调用(如 `getMediaFile()`仍然使用已销毁的SDK
- 底层C++库访问无效指针导致SIGSEGV错误
3. **多线程并发问题**
- 在多线程环境下一个线程销毁SDK后
- 其他线程仍在使用该SDK导致崩溃
## 解决方案
**4.8.0** 版本开始WxJava提供了新的安全API完全由框架管理SDK生命周期。
### 新API列表
| 旧API已废弃 | 新API推荐使用 | 说明 |
|----------------|------------------|------|
| `getChatDatas()` | `getChatRecords()` | 拉取聊天记录不暴露SDK |
| `getDecryptData(sdk, ...)` | `getDecryptChatData(...)` | 解密聊天数据无需传入SDK |
| `getChatPlainText(sdk, ...)` | `getChatRecordPlainText(...)` | 获取明文数据无需传入SDK |
| `getMediaFile(sdk, ...)` | `downloadMediaFile(...)` | 下载媒体文件无需传入SDK |
### 使用示例
#### 错误用法旧API已废弃
```java
// ❌ 不推荐容易导致JVM崩溃
WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService();
// 拉取聊天记录
WxCpChatDatas chatDatas = msgAuditService.getChatDatas(seq, 1000L, null, null, 1000L);
for (WxCpChatDatas.WxCpChatData chatData : chatDatas.getChatData()) {
// 解密数据
WxCpChatModel model = msgAuditService.getDecryptData(chatDatas.getSdk(), chatData, 2);
// 下载媒体文件
if ("image".equals(model.getMsgType())) {
String sdkFileId = model.getImage().getSdkFileId();
msgAuditService.getMediaFile(chatDatas.getSdk(), sdkFileId, null, null, 1000L, targetPath);
}
}
// ❌ 危险操作手动销毁SDK可能导致后续调用崩溃
Finance.DestroySdk(chatDatas.getSdk());
```
#### 正确用法新API推荐
```java
// ✅ 推荐SDK生命周期由框架自动管理安全可靠
WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService();
// 拉取聊天记录不返回SDK
List<WxCpChatDatas.WxCpChatData> chatRecords =
msgAuditService.getChatRecords(seq, 1000L, null, null, 1000L);
for (WxCpChatDatas.WxCpChatData chatData : chatRecords) {
// 解密数据无需传入SDK
WxCpChatModel model = msgAuditService.getDecryptChatData(chatData, 2);
// 下载媒体文件无需传入SDK
if ("image".equals(model.getMsgType())) {
String sdkFileId = model.getImage().getSdkFileId();
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath);
}
}
// ✅ 无需手动销毁SDK框架会自动管理
```
### 完整示例:拉取并处理会话存档
```java
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.WxCpMsgAuditService;
import me.chanjar.weixin.cp.bean.msgaudit.WxCpChatDatas;
import me.chanjar.weixin.cp.bean.msgaudit.WxCpChatModel;
import me.chanjar.weixin.cp.constant.WxCpConsts;
import java.util.List;
public class MsgAuditExample {
private final WxCpService wxCpService;
public void processMessages(long seq) throws Exception {
WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService();
// 拉取聊天记录
List<WxCpChatDatas.WxCpChatData> chatRecords =
msgAuditService.getChatRecords(seq, 1000L, null, null, 1000L);
for (WxCpChatDatas.WxCpChatData chatData : chatRecords) {
seq = chatData.getSeq();
// 获取明文数据
String plainText = msgAuditService.getChatRecordPlainText(chatData, 2);
WxCpChatModel model = WxCpChatModel.fromJson(plainText);
// 处理不同类型的消息
switch (model.getMsgType()) {
case WxCpConsts.MsgAuditMediaType.TEXT:
processTextMessage(model);
break;
case WxCpConsts.MsgAuditMediaType.IMAGE:
processImageMessage(model, msgAuditService);
break;
case WxCpConsts.MsgAuditMediaType.FILE:
processFileMessage(model, msgAuditService);
break;
default:
// 处理其他类型消息
break;
}
}
}
private void processTextMessage(WxCpChatModel model) {
String content = model.getText().getContent();
System.out.println("文本消息:" + content);
}
private void processImageMessage(WxCpChatModel model, WxCpMsgAuditService msgAuditService)
throws Exception {
String sdkFileId = model.getImage().getSdkFileId();
String md5Sum = model.getImage().getMd5Sum();
String targetPath = "/path/to/save/" + md5Sum + ".jpg";
// 下载图片无需传入SDK
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath);
System.out.println("图片已保存:" + targetPath);
}
private void processFileMessage(WxCpChatModel model, WxCpMsgAuditService msgAuditService)
throws Exception {
String sdkFileId = model.getFile().getSdkFileId();
String fileName = model.getFile().getFileName();
String targetPath = "/path/to/save/" + fileName;
// 下载文件无需传入SDK
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath);
System.out.println("文件已保存:" + targetPath);
}
}
```
### 使用Lambda处理媒体文件流
新API同样支持使用Lambda表达式处理媒体文件的数据流
```java
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, data -> {
try {
// 处理每个数据分片(大文件会分片传输)
// 例如:上传到云存储、写入数据库等
uploadToCloud(data);
} catch (Exception e) {
e.printStackTrace();
}
});
```
## 技术实现原理
### 引用计数机制
新API在内部实现了SDK引用计数机制
1. **获取SDK时**:引用计数 +1
2. **使用完成后**:引用计数 -1
3. **计数归零时**SDK被自动释放
```java
// 框架内部实现(简化版)
public void downloadMediaFile(String sdkFileId, ...) {
long sdk = initSdk(); // 获取或初始化SDK
configStorage.incrementMsgAuditSdkRefCount(sdk); // 引用计数 +1
try {
// 执行实际操作
getMediaFile(sdk, sdkFileId, ...);
} finally {
// 确保引用计数一定会减少
configStorage.decrementMsgAuditSdkRefCount(sdk); // 引用计数 -1
}
}
```
### SDK缓存机制
SDK初始化后会缓存7200秒企业微信官方文档规定避免频繁初始化
- **首次调用**初始化新的SDK
- **7200秒内**复用缓存的SDK
- **超过7200秒**重新初始化SDK
新API的引用计数机制与缓存机制完美配合确保SDK不会被提前销毁。
## 迁移指南
### 第一步使用新API替换旧API
查找代码中的旧API调用
```java
// 查找模式
getChatDatas(
getDecryptData(.*sdk
getChatPlainText(.*sdk
getMediaFile(.*sdk
Finance.DestroySdk(
```
替换为对应的新API参考前面的对照表
### 第二步移除手动SDK管理代码
删除所有 `Finance.DestroySdk()` 调用SDK生命周期由框架自动管理。
### 第三步:测试验证
1. 在测试环境验证新API功能正常
2. 观察日志确认没有SDK相关的错误
3. 进行压力测试,验证多线程环境下的稳定性
## 常见问题
### Q1: 旧代码会立即停止工作吗?
**A:** 不会。旧API被标记为 `@Deprecated`但仍然可用只是不推荐继续使用。建议尽快迁移到新API以避免潜在问题。
### Q2: 如何知道SDK是否被正确释放
**A:** 框架会自动管理SDK生命周期开发者无需关心。如果需要调试可以查看配置存储中的引用计数。
### Q3: 多线程环境下新API安全吗
**A:** 是的。新API使用了引用计数机制配合 `synchronized` 关键字,确保多线程环境下的安全性。
### Q4: 性能会受影响吗?
**A:** 不会。新API在实现上增加了引用计数的开销但这是轻量级的操作原子操作对性能影响可以忽略不计。SDK缓存机制保持不变。
### Q5: 可以同时使用新旧API吗
**A:** 技术上可以但强烈不推荐。混用可能导致SDK生命周期管理混乱建议统一使用新API。
## 相关链接
- [企业微信会话存档官方文档](https://developer.work.weixin.qq.com/document/path/91360)
- [WxJava GitHub 仓库](https://github.com/binarywang/WxJava)
- [问题反馈](https://github.com/binarywang/WxJava/issues)
## 版本要求
- **最低版本**: 4.8.0
- **推荐版本**: 最新版本
## 反馈与支持
如果在使用过程中遇到问题,请:
1. 查看本文档的常见问题部分
2. 在 GitHub 上提交 Issue
3. 加入微信群获取社区支持
---
**最后更新时间**: 2026-01-14

View File

@@ -28,9 +28,26 @@ public interface WxCpMsgAuditService {
* @param timeout 超时时间,根据实际需要填写
* @return 返回是否调用成功 chat datas
* @throws Exception the exception
* @deprecated 请使用 {@link #getChatRecords(long, long, String, String, long)} 代替,
* 该方法会将SDK暴露给调用方容易导致SDK生命周期管理混乱引发JVM崩溃
*/
@Deprecated
WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception;
/**
* 拉取聊天记录函数(推荐使用)
* 该方法不会将SDK暴露给调用方SDK生命周期由框架自动管理更加安全
*
* @param seq 从指定的seq开始拉取消息注意的是返回的消息从seq+1开始返回seq为之前接口返回的最大seq值。首次使用请使用seq:0
* @param limit 一次拉取的消息条数最大值1000条超过1000条会返回错误
* @param proxy 使用代理的请求需要传入代理的链接。如socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081如果没有传null
* @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123如果没有传null
* @param timeout 超时时间,根据实际需要填写
* @return 返回聊天记录列表不包含SDK信息
* @throws Exception the exception
*/
List<WxCpChatDatas.WxCpChatData> getChatRecords(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception;
/**
* 获取解密的聊天数据Model
*
@@ -39,10 +56,24 @@ public interface WxCpMsgAuditService {
* @param pkcs1 使用什么方式进行解密1代表使用PKCS1进行解密2代表PKCS8进行解密 ...
* @return 解密后的聊天数据 decrypt data
* @throws Exception the exception
* @deprecated 请使用 {@link #getDecryptChatData(WxCpChatDatas.WxCpChatData, Integer)} 代替,
* 该方法需要传入SDK容易导致SDK生命周期管理混乱引发JVM崩溃
*/
@Deprecated
WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData,
@NonNull Integer pkcs1) throws Exception;
/**
* 获取解密的聊天数据Model推荐使用
* 该方法不需要传入SDKSDK由框架自动管理更加安全
*
* @param chatData 聊天数据
* @param pkcs1 使用什么方式进行解密1代表使用PKCS1进行解密2代表PKCS8进行解密 ...
* @return 解密后的聊天数据
* @throws Exception the exception
*/
WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception;
/**
* 获取解密的聊天数据明文
*
@@ -51,9 +82,23 @@ public interface WxCpMsgAuditService {
* @param pkcs1 使用什么方式进行解密1代表使用PKCS1进行解密2代表PKCS8进行解密 ...
* @return 解密后的明文 chat plain text
* @throws Exception the exception
* @deprecated 请使用 {@link #getChatRecordPlainText(WxCpChatDatas.WxCpChatData, Integer)} 代替,
* 该方法需要传入SDK容易导致SDK生命周期管理混乱引发JVM崩溃
*/
@Deprecated
String getChatPlainText(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception;
/**
* 获取解密的聊天数据明文(推荐使用)
* 该方法不需要传入SDKSDK由框架自动管理更加安全
*
* @param chatData 聊天数据
* @param pkcs1 使用什么方式进行解密1代表使用PKCS1进行解密2代表PKCS8进行解密 ...
* @return 解密后的明文
* @throws Exception the exception
*/
String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception;
/**
* 获取媒体文件
* 针对图片、文件等媒体数据提供sdk接口拉取数据内容。
@@ -69,10 +114,32 @@ public interface WxCpMsgAuditService {
* @param timeout 超时时间分片数据需累加到文件存储。单次最大返回512K字节如果文件比较大自行设置长一点比如timeout=10000
* @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif
* @throws WxErrorException the wx error exception
* @deprecated 请使用 {@link #downloadMediaFile(String, String, String, long, String)} 代替,
* 该方法需要传入SDK容易导致SDK生命周期管理混乱引发JVM崩溃
*/
@Deprecated
void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull String targetFilePath) throws WxErrorException;
/**
* 获取媒体文件(推荐使用)
* 该方法不需要传入SDKSDK由框架自动管理更加安全
* 针对图片、文件等媒体数据提供sdk接口拉取数据内容。
* <p>
* 注意:
* 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。
* 详情可以看官方文档,亦可阅读此接口源码。
*
* @param sdkfileid 消息体内容中的sdkfileid信息
* @param proxy 使用代理的请求需要传入代理的链接。如socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081如果没有传null
* @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123如果没有传null
* @param timeout 超时时间分片数据需累加到文件存储。单次最大返回512K字节如果文件比较大自行设置长一点比如timeout=10000
* @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif
* @throws WxErrorException the wx error exception
*/
void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull String targetFilePath) throws WxErrorException;
/**
* 获取媒体文件 传入一个lambdaeach所有的数据分片byte[],更加灵活
* 针对图片、文件等媒体数据提供sdk接口拉取数据内容。
@@ -85,10 +152,29 @@ public interface WxCpMsgAuditService {
* @param timeout 超时时间分片数据需累加到文件存储。单次最大返回512K字节如果文件比较大自行设置长一点比如timeout=10000
* @param action 传入一个lambdaeach所有的数据分片
* @throws WxErrorException the wx error exception
* @deprecated 请使用 {@link #downloadMediaFile(String, String, String, long, Consumer)} 代替,
* 该方法需要传入SDK容易导致SDK生命周期管理混乱引发JVM崩溃
*/
@Deprecated
void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull Consumer<byte[]> action) throws WxErrorException;
/**
* 获取媒体文件 传入一个lambdaeach所有的数据分片byte[],更加灵活(推荐使用)
* 该方法不需要传入SDKSDK由框架自动管理更加安全
* 针对图片、文件等媒体数据提供sdk接口拉取数据内容。
* 详情可以看官方文档,亦可阅读此接口源码。
*
* @param sdkfileid 消息体内容中的sdkfileid信息
* @param proxy 使用代理的请求需要传入代理的链接。如socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081如果没有传null
* @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123如果没有传null
* @param timeout 超时时间分片数据需累加到文件存储。单次最大返回512K字节如果文件比较大自行设置长一点比如timeout=10000
* @param action 传入一个lambdaeach所有的数据分片
* @throws WxErrorException the wx error exception
*/
void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull Consumer<byte[]> action) throws WxErrorException;
/**
* 获取会话内容存档开启成员列表
* 企业可通过此接口,获取企业开启会话内容存档的成员列表

View File

@@ -20,6 +20,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
@@ -137,6 +138,49 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
return sdk;
}
/**
* 获取SDK并增加引用计数原子操作
* 如果SDK未初始化或已过期会自动初始化
*
* @return sdk id
* @throws WxErrorException 初始化失败时抛出异常
*/
private long acquireSdk() throws WxErrorException {
WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage();
// 尝试获取现有的有效SDK并增加引用计数原子操作
long sdk = configStorage.acquireMsgAuditSdk();
if (sdk > 0) {
// 成功获取到有效的SDK
return sdk;
}
// SDK未初始化或已过期需要初始化
// initSdk()方法已经是synchronized的确保只有一个线程初始化
sdk = this.initSdk();
// 初始化后增加引用计数
int refCount = configStorage.incrementMsgAuditSdkRefCount(sdk);
if (refCount < 0) {
// SDK已经被替换需要重新获取
return acquireSdk();
}
return sdk;
}
/**
* 释放SDK引用计数
*
* @param sdk sdk id
*/
private void releaseSdk(long sdk) {
if (sdk > 0) {
cpService.getWxCpConfigStorage().releaseMsgAuditSdk(sdk);
}
}
@Override
public WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData,
@NonNull Integer pkcs1) throws Exception {
@@ -280,4 +324,127 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
return WxCpAgreeInfo.fromJson(responseContent);
}
@Override
public List<WxCpChatDatas.WxCpChatData> getChatRecords(long seq, @NonNull long limit, String proxy, String passwd,
@NonNull long timeout) throws Exception {
// 获取SDK并自动增加引用计数原子操作
long sdk = this.acquireSdk();
try {
long slice = Finance.NewSlice();
long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
if (ret != 0) {
Finance.FreeSlice(slice);
throw new WxErrorException("getchatdata err ret " + ret);
}
// 拉取会话存档
String content = Finance.GetContentFromSlice(slice);
Finance.FreeSlice(slice);
WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content);
if (chatDatas.getErrCode().intValue() != 0) {
throw new WxErrorException(chatDatas.toJson());
}
List<WxCpChatDatas.WxCpChatData> chatDataList = chatDatas.getChatData();
return chatDataList != null ? chatDataList : Collections.emptyList();
} finally {
// 释放SDK引用计数原子操作
this.releaseSdk(sdk);
}
}
@Override
public WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData,
@NonNull Integer pkcs1) throws Exception {
// 获取SDK并自动增加引用计数原子操作
long sdk = this.acquireSdk();
try {
String plainText = this.decryptChatData(sdk, chatData, pkcs1);
return WxCpChatModel.fromJson(plainText);
} finally {
// 释放SDK引用计数原子操作
this.releaseSdk(sdk);
}
}
@Override
public String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData,
@NonNull Integer pkcs1) throws Exception {
// 获取SDK并自动增加引用计数原子操作
long sdk = this.acquireSdk();
try {
return this.decryptChatData(sdk, chatData, pkcs1);
} finally {
// 释放SDK引用计数原子操作
this.releaseSdk(sdk);
}
}
@Override
public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull String targetFilePath) throws WxErrorException {
// 获取SDK并自动增加引用计数原子操作
long sdk;
try {
sdk = this.acquireSdk();
} catch (Exception e) {
throw new WxErrorException(e);
}
// 使用AtomicReference捕获Lambda中的异常以便在执行完后抛出
final java.util.concurrent.atomic.AtomicReference<Exception> exceptionHolder = new java.util.concurrent.atomic.AtomicReference<>();
try {
File targetFile = new File(targetFilePath);
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> {
// 如果之前已经发生异常,不再继续处理
if (exceptionHolder.get() != null) {
return;
}
try {
// 大于512k的文件会分片拉取此处需要使用追加写避免后面的分片覆盖之前的数据。
FileOutputStream outputStream = new FileOutputStream(targetFile, true);
outputStream.write(i);
outputStream.close();
} catch (Exception e) {
exceptionHolder.set(e);
}
});
// 检查是否发生异常,如果有则抛出
Exception caughtException = exceptionHolder.get();
if (caughtException != null) {
throw new WxErrorException(caughtException);
}
} finally {
// 释放SDK引用计数原子操作
this.releaseSdk(sdk);
}
}
@Override
public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull Consumer<byte[]> action) throws WxErrorException {
// 获取SDK并自动增加引用计数原子操作
long sdk;
try {
sdk = this.acquireSdk();
} catch (Exception e) {
throw new WxErrorException(e);
}
try {
this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, action);
} finally {
// 释放SDK引用计数原子操作
this.releaseSdk(sdk);
}
}
}

View File

@@ -292,4 +292,47 @@ public interface WxCpConfigStorage {
* 使会话存档SDK过期
*/
void expireMsgAuditSdk();
/**
* 增加会话存档SDK的引用计数
* 用于支持多线程安全的SDK生命周期管理
*
* @param sdk sdk id
* @return 增加后的引用计数如果SDK不匹配返回-1
*/
int incrementMsgAuditSdkRefCount(long sdk);
/**
* 减少会话存档SDK的引用计数
* 当引用计数降为0时自动销毁SDK以释放资源
*
* @param sdk sdk id
* @return 减少后的引用计数如果返回0表示SDK已被销毁如果SDK不匹配返回-1
*/
int decrementMsgAuditSdkRefCount(long sdk);
/**
* 获取会话存档SDK的引用计数
*
* @param sdk sdk id
* @return 当前引用计数如果SDK不匹配返回-1
*/
int getMsgAuditSdkRefCount(long sdk);
/**
* 获取当前SDK并增加引用计数原子操作
* 如果SDK未初始化或已过期返回0而不增加引用计数
* 此方法用于在获取SDK后立即增加引用计数避免并发问题
*
* @return 当前有效的SDK id并已增加引用计数如果SDK无效返回0
*/
long acquireMsgAuditSdk();
/**
* 减少SDK引用计数并在必要时释放原子操作
* 此方法确保引用计数递减和SDK检查在同一个同步块内完成
*
* @param sdk sdk id
*/
void releaseMsgAuditSdk(long sdk);
}

View File

@@ -1,5 +1,6 @@
package me.chanjar.weixin.cp.config.impl;
import com.tencent.wework.Finance;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.cp.config.WxCpConfigStorage;
@@ -54,6 +55,10 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
*/
private volatile long msgAuditSdk;
private volatile long msgAuditSdkExpiresTime;
/**
* 会话存档SDK引用计数用于多线程安全的生命周期管理
*/
private volatile int msgAuditSdkRefCount;
private volatile String oauth2redirectUri;
private volatile String httpProxyHost;
private volatile int httpProxyPort;
@@ -470,13 +475,77 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
@Override
public synchronized void updateMsgAuditSdk(long sdk, int expiresInSeconds) {
// 如果有旧的SDK且不同于新的SDK需要销毁旧的SDK
if (this.msgAuditSdk > 0 && this.msgAuditSdk != sdk) {
// 无论旧SDK是否仍有引用都需要销毁它以避免资源泄漏
// 如果有飞行中的请求使用旧SDK这些请求可能会失败但这比资源泄漏更安全
Finance.DestroySdk(this.msgAuditSdk);
}
this.msgAuditSdk = sdk;
// 预留200秒的时间
this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
// 重置引用计数因为这是一个全新的SDK
this.msgAuditSdkRefCount = 0;
}
@Override
public void expireMsgAuditSdk() {
this.msgAuditSdkExpiresTime = 0;
}
@Override
public synchronized int incrementMsgAuditSdkRefCount(long sdk) {
if (this.msgAuditSdk == sdk && sdk > 0) {
return ++this.msgAuditSdkRefCount;
}
return -1; // SDK不匹配返回-1表示错误
}
@Override
public synchronized int decrementMsgAuditSdkRefCount(long sdk) {
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
int newCount = --this.msgAuditSdkRefCount;
// 当引用计数降为0时自动销毁SDK以释放资源
// 再次检查SDK是否仍然是当前缓存的SDK防止并发重新初始化
if (newCount == 0 && this.msgAuditSdk == sdk) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
}
return newCount;
}
return -1; // SDK不匹配或引用计数已为0返回-1表示错误
}
@Override
public synchronized int getMsgAuditSdkRefCount(long sdk) {
if (this.msgAuditSdk == sdk && sdk > 0) {
return this.msgAuditSdkRefCount;
}
return -1; // SDK不匹配返回-1表示错误
}
@Override
public synchronized long acquireMsgAuditSdk() {
// 检查SDK是否有效已初始化且未过期
if (this.msgAuditSdk > 0 && !isMsgAuditSdkExpired()) {
this.msgAuditSdkRefCount++;
return this.msgAuditSdk;
}
return 0; // SDK未初始化或已过期
}
@Override
public synchronized void releaseMsgAuditSdk(long sdk) {
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
int newCount = --this.msgAuditSdkRefCount;
// 当引用计数降为0时自动销毁SDK以释放资源
// 再次检查SDK是否仍然是当前缓存的SDK防止并发重新初始化
if (newCount == 0 && this.msgAuditSdk == sdk) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
}
}
}
}

View File

@@ -1,5 +1,6 @@
package me.chanjar.weixin.cp.config.impl;
import com.tencent.wework.Finance;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.cp.config.WxCpConfigStorage;
@@ -55,6 +56,10 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
*/
private volatile long msgAuditSdk;
private volatile long msgAuditSdkExpiresTime;
/**
* 会话存档SDK引用计数用于多线程安全的生命周期管理
*/
private volatile int msgAuditSdkRefCount;
/**
* Instantiates a new Wx cp redis config.
@@ -488,13 +493,77 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
@Override
public synchronized void updateMsgAuditSdk(long sdk, int expiresInSeconds) {
// 如果有旧的SDK且不同于新的SDK需要销毁旧的SDK
if (this.msgAuditSdk > 0 && this.msgAuditSdk != sdk) {
// 无论旧SDK是否仍有引用都需要销毁它以避免资源泄漏
// 如果有飞行中的请求使用旧SDK这些请求可能会失败但这比资源泄漏更安全
Finance.DestroySdk(this.msgAuditSdk);
}
this.msgAuditSdk = sdk;
// 预留200秒的时间
this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
// 重置引用计数因为这是一个全新的SDK
this.msgAuditSdkRefCount = 0;
}
@Override
public void expireMsgAuditSdk() {
this.msgAuditSdkExpiresTime = 0;
}
@Override
public synchronized int incrementMsgAuditSdkRefCount(long sdk) {
if (this.msgAuditSdk == sdk && sdk > 0) {
return ++this.msgAuditSdkRefCount;
}
return -1; // SDK不匹配返回-1表示错误
}
@Override
public synchronized int decrementMsgAuditSdkRefCount(long sdk) {
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
int newCount = --this.msgAuditSdkRefCount;
// 当引用计数降为0时自动销毁SDK以释放资源
// 再次检查SDK是否仍然是当前缓存的SDK防止并发重新初始化
if (newCount == 0 && this.msgAuditSdk == sdk) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
}
return newCount;
}
return -1; // SDK不匹配或引用计数已为0返回-1表示错误
}
@Override
public synchronized int getMsgAuditSdkRefCount(long sdk) {
if (this.msgAuditSdk == sdk && sdk > 0) {
return this.msgAuditSdkRefCount;
}
return -1; // SDK不匹配返回-1表示错误
}
@Override
public synchronized long acquireMsgAuditSdk() {
// 检查SDK是否有效已初始化且未过期
if (this.msgAuditSdk > 0 && !isMsgAuditSdkExpired()) {
this.msgAuditSdkRefCount++;
return this.msgAuditSdk;
}
return 0; // SDK未初始化或已过期
}
@Override
public synchronized void releaseMsgAuditSdk(long sdk) {
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
int newCount = --this.msgAuditSdkRefCount;
// 当引用计数降为0时自动销毁SDK以释放资源
// 再次检查SDK是否仍然是当前缓存的SDK防止并发重新初始化
if (newCount == 0 && this.msgAuditSdk == sdk) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
}
}
}
}

View File

@@ -754,6 +754,84 @@ public class WxCpMsgAuditTest {
Finance.DestroySdk(chatDatas.getSdk());
}
/**
* 测试新的安全API方法推荐使用
* 这些方法不需要手动管理SDK生命周期更加安全
*/
@Test
public void testNewSafeApi() throws Exception {
WxCpMsgAuditService msgAuditService = cpService.getMsgAuditService();
// 测试新的getChatRecords方法 - 不暴露SDK
List<WxCpChatDatas.WxCpChatData> chatRecords = msgAuditService.getChatRecords(0L, 10L, null, null, 1000L);
log.info("获取到 {} 条聊天记录", chatRecords.size());
for (WxCpChatDatas.WxCpChatData chatData : chatRecords) {
// 测试新的getDecryptChatData方法 - 不需要传入SDK
WxCpChatModel decryptData = msgAuditService.getDecryptChatData(chatData, 2);
log.info("解密数据:{}", decryptData.toJson());
// 测试新的getChatRecordPlainText方法 - 不需要传入SDK
String plainText = msgAuditService.getChatRecordPlainText(chatData, 2);
log.info("明文数据:{}", plainText);
// 如果是媒体消息测试新的downloadMediaFile方法
String msgType = decryptData.getMsgType();
if ("image".equals(msgType) || "voice".equals(msgType) || "video".equals(msgType) || "file".equals(msgType)) {
String suffix = "";
String md5Sum = "";
String sdkFileId = "";
switch (msgType) {
case "image":
suffix = ".jpg";
md5Sum = decryptData.getImage().getMd5Sum();
sdkFileId = decryptData.getImage().getSdkFileId();
break;
case "voice":
suffix = ".amr";
md5Sum = decryptData.getVoice().getMd5Sum();
sdkFileId = decryptData.getVoice().getSdkFileId();
break;
case "video":
suffix = ".mp4";
md5Sum = decryptData.getVideo().getMd5Sum();
sdkFileId = decryptData.getVideo().getSdkFileId();
break;
case "file":
md5Sum = decryptData.getFile().getMd5Sum();
suffix = "." + decryptData.getFile().getFileExt();
sdkFileId = decryptData.getFile().getSdkFileId();
break;
default:
// 未知消息类型,跳过处理
continue;
}
// 测试新的downloadMediaFile方法 - 不需要传入SDK
String path = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String targetPath = path + "testfile-new/" + md5Sum + suffix;
File file = new File(targetPath);
// 确保父目录存在
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
// 删除已存在的文件
if (file.exists()) {
file.delete();
}
// 使用新的API下载媒体文件
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath);
log.info("媒体文件下载成功:{}", targetPath);
}
}
// 注意使用新API无需手动调用 Finance.DestroySdk()SDK由框架自动管理
}
// 测试Uint64类型
public static void main(String[] args){
/*