Skip to content

Commit 47b4431

Browse files
authored
🎨 #3848 【企业微信】修复会话存档SDK生命周期管理导致的JVM崩溃问题
1 parent 30914f3 commit 47b4431

File tree

7 files changed

+807
-0
lines changed

7 files changed

+807
-0
lines changed
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# 企业微信会话存档SDK安全使用指南
2+
3+
## 问题背景
4+
5+
在使用企业微信会话存档功能时,部分开发者遇到了JVM崩溃的问题。典型错误信息如下:
6+
7+
```
8+
SIGSEGV (0xb) at pc=0x00007fcd50460d93
9+
Problematic frame:
10+
C [libWeWorkFinanceSdk_Java.so+0x260d93] WeWorkFinanceSdk::TryRefresh(std::string const&, std::string const&, int)+0x23
11+
```
12+
13+
## 问题原因
14+
15+
旧版API设计存在以下问题:
16+
17+
1. **SDK生命周期管理混乱**
18+
- `getChatDatas()` 方法会返回SDK实例给调用方
19+
- 开发者需要手动调用 `Finance.DestroySdk()` 来销毁SDK
20+
- 但SDK在框架内部有7200秒的缓存机制
21+
22+
2. **手动销毁导致缓存失效**
23+
- 当开发者手动销毁SDK后,框架缓存中的SDK引用变为无效
24+
- 后续调用(如 `getMediaFile()`)仍然使用已销毁的SDK
25+
- 底层C++库访问无效指针,导致SIGSEGV错误
26+
27+
3. **多线程并发问题**
28+
- 在多线程环境下,一个线程销毁SDK后
29+
- 其他线程仍在使用该SDK,导致崩溃
30+
31+
## 解决方案
32+
33+
**4.8.0** 版本开始,WxJava提供了新的安全API,完全由框架管理SDK生命周期。
34+
35+
### 新API列表
36+
37+
| 旧API(已废弃) | 新API(推荐使用) | 说明 |
38+
|----------------|------------------|------|
39+
| `getChatDatas()` | `getChatRecords()` | 拉取聊天记录,不暴露SDK |
40+
| `getDecryptData(sdk, ...)` | `getDecryptChatData(...)` | 解密聊天数据,无需传入SDK |
41+
| `getChatPlainText(sdk, ...)` | `getChatRecordPlainText(...)` | 获取明文数据,无需传入SDK |
42+
| `getMediaFile(sdk, ...)` | `downloadMediaFile(...)` | 下载媒体文件,无需传入SDK |
43+
44+
### 使用示例
45+
46+
#### 错误用法(旧API,已废弃)
47+
48+
```java
49+
// ❌ 不推荐:容易导致JVM崩溃
50+
WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService();
51+
52+
// 拉取聊天记录
53+
WxCpChatDatas chatDatas = msgAuditService.getChatDatas(seq, 1000L, null, null, 1000L);
54+
55+
for (WxCpChatDatas.WxCpChatData chatData : chatDatas.getChatData()) {
56+
// 解密数据
57+
WxCpChatModel model = msgAuditService.getDecryptData(chatDatas.getSdk(), chatData, 2);
58+
59+
// 下载媒体文件
60+
if ("image".equals(model.getMsgType())) {
61+
String sdkFileId = model.getImage().getSdkFileId();
62+
msgAuditService.getMediaFile(chatDatas.getSdk(), sdkFileId, null, null, 1000L, targetPath);
63+
}
64+
}
65+
66+
// ❌ 危险操作:手动销毁SDK可能导致后续调用崩溃
67+
Finance.DestroySdk(chatDatas.getSdk());
68+
```
69+
70+
#### 正确用法(新API,推荐)
71+
72+
```java
73+
// ✅ 推荐:SDK生命周期由框架自动管理,安全可靠
74+
WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService();
75+
76+
// 拉取聊天记录(不返回SDK)
77+
List<WxCpChatDatas.WxCpChatData> chatRecords =
78+
msgAuditService.getChatRecords(seq, 1000L, null, null, 1000L);
79+
80+
for (WxCpChatDatas.WxCpChatData chatData : chatRecords) {
81+
// 解密数据(无需传入SDK)
82+
WxCpChatModel model = msgAuditService.getDecryptChatData(chatData, 2);
83+
84+
// 下载媒体文件(无需传入SDK)
85+
if ("image".equals(model.getMsgType())) {
86+
String sdkFileId = model.getImage().getSdkFileId();
87+
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath);
88+
}
89+
}
90+
91+
// ✅ 无需手动销毁SDK,框架会自动管理
92+
```
93+
94+
### 完整示例:拉取并处理会话存档
95+
96+
```java
97+
import me.chanjar.weixin.cp.api.WxCpService;
98+
import me.chanjar.weixin.cp.api.WxCpMsgAuditService;
99+
import me.chanjar.weixin.cp.bean.msgaudit.WxCpChatDatas;
100+
import me.chanjar.weixin.cp.bean.msgaudit.WxCpChatModel;
101+
import me.chanjar.weixin.cp.constant.WxCpConsts;
102+
103+
import java.util.List;
104+
105+
public class MsgAuditExample {
106+
107+
private final WxCpService wxCpService;
108+
109+
public void processMessages(long seq) throws Exception {
110+
WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService();
111+
112+
// 拉取聊天记录
113+
List<WxCpChatDatas.WxCpChatData> chatRecords =
114+
msgAuditService.getChatRecords(seq, 1000L, null, null, 1000L);
115+
116+
for (WxCpChatDatas.WxCpChatData chatData : chatRecords) {
117+
seq = chatData.getSeq();
118+
119+
// 获取明文数据
120+
String plainText = msgAuditService.getChatRecordPlainText(chatData, 2);
121+
WxCpChatModel model = WxCpChatModel.fromJson(plainText);
122+
123+
// 处理不同类型的消息
124+
switch (model.getMsgType()) {
125+
case WxCpConsts.MsgAuditMediaType.TEXT:
126+
processTextMessage(model);
127+
break;
128+
129+
case WxCpConsts.MsgAuditMediaType.IMAGE:
130+
processImageMessage(model, msgAuditService);
131+
break;
132+
133+
case WxCpConsts.MsgAuditMediaType.FILE:
134+
processFileMessage(model, msgAuditService);
135+
break;
136+
137+
default:
138+
// 处理其他类型消息
139+
break;
140+
}
141+
}
142+
}
143+
144+
private void processTextMessage(WxCpChatModel model) {
145+
String content = model.getText().getContent();
146+
System.out.println("文本消息:" + content);
147+
}
148+
149+
private void processImageMessage(WxCpChatModel model, WxCpMsgAuditService msgAuditService)
150+
throws Exception {
151+
String sdkFileId = model.getImage().getSdkFileId();
152+
String md5Sum = model.getImage().getMd5Sum();
153+
String targetPath = "/path/to/save/" + md5Sum + ".jpg";
154+
155+
// 下载图片(无需传入SDK)
156+
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath);
157+
System.out.println("图片已保存:" + targetPath);
158+
}
159+
160+
private void processFileMessage(WxCpChatModel model, WxCpMsgAuditService msgAuditService)
161+
throws Exception {
162+
String sdkFileId = model.getFile().getSdkFileId();
163+
String fileName = model.getFile().getFileName();
164+
String targetPath = "/path/to/save/" + fileName;
165+
166+
// 下载文件(无需传入SDK)
167+
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath);
168+
System.out.println("文件已保存:" + targetPath);
169+
}
170+
}
171+
```
172+
173+
### 使用Lambda处理媒体文件流
174+
175+
新API同样支持使用Lambda表达式处理媒体文件的数据流:
176+
177+
```java
178+
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, data -> {
179+
try {
180+
// 处理每个数据分片(大文件会分片传输)
181+
// 例如:上传到云存储、写入数据库等
182+
uploadToCloud(data);
183+
} catch (Exception e) {
184+
e.printStackTrace();
185+
}
186+
});
187+
```
188+
189+
## 技术实现原理
190+
191+
### 引用计数机制
192+
193+
新API在内部实现了SDK引用计数机制:
194+
195+
1. **获取SDK时**:引用计数 +1
196+
2. **使用完成后**:引用计数 -1
197+
3. **计数归零时**:SDK被自动释放
198+
199+
```java
200+
// 框架内部实现(简化版)
201+
public void downloadMediaFile(String sdkFileId, ...) {
202+
long sdk = initSdk(); // 获取或初始化SDK
203+
configStorage.incrementMsgAuditSdkRefCount(sdk); // 引用计数 +1
204+
205+
try {
206+
// 执行实际操作
207+
getMediaFile(sdk, sdkFileId, ...);
208+
} finally {
209+
// 确保引用计数一定会减少
210+
configStorage.decrementMsgAuditSdkRefCount(sdk); // 引用计数 -1
211+
}
212+
}
213+
```
214+
215+
### SDK缓存机制
216+
217+
SDK初始化后会缓存7200秒(企业微信官方文档规定),避免频繁初始化:
218+
219+
- **首次调用**:初始化新的SDK
220+
- **7200秒内**:复用缓存的SDK
221+
- **超过7200秒**:重新初始化SDK
222+
223+
新API的引用计数机制与缓存机制完美配合,确保SDK不会被提前销毁。
224+
225+
## 迁移指南
226+
227+
### 第一步:使用新API替换旧API
228+
229+
查找代码中的旧API调用:
230+
231+
```java
232+
// 查找模式
233+
getChatDatas(
234+
getDecryptData(.*sdk
235+
getChatPlainText(.*sdk
236+
getMediaFile(.*sdk
237+
Finance.DestroySdk(
238+
```
239+
240+
替换为对应的新API(参考前面的对照表)。
241+
242+
### 第二步:移除手动SDK管理代码
243+
244+
删除所有 `Finance.DestroySdk()` 调用,SDK生命周期由框架自动管理。
245+
246+
### 第三步:测试验证
247+
248+
1. 在测试环境验证新API功能正常
249+
2. 观察日志,确认没有SDK相关的错误
250+
3. 进行压力测试,验证多线程环境下的稳定性
251+
252+
## 常见问题
253+
254+
### Q1: 旧代码会立即停止工作吗?
255+
256+
**A:** 不会。旧API被标记为 `@Deprecated`,但仍然可用,只是不推荐继续使用。建议尽快迁移到新API以避免潜在问题。
257+
258+
### Q2: 如何知道SDK是否被正确释放?
259+
260+
**A:** 框架会自动管理SDK生命周期,开发者无需关心。如果需要调试,可以查看配置存储中的引用计数。
261+
262+
### Q3: 多线程环境下新API安全吗?
263+
264+
**A:** 是的。新API使用了引用计数机制,配合 `synchronized` 关键字,确保多线程环境下的安全性。
265+
266+
### Q4: 性能会受影响吗?
267+
268+
**A:** 不会。新API在实现上增加了引用计数的开销,但这是轻量级的操作(原子操作),对性能影响可以忽略不计。SDK缓存机制保持不变。
269+
270+
### Q5: 可以同时使用新旧API吗?
271+
272+
**A:** 技术上可以,但强烈不推荐。混用可能导致SDK生命周期管理混乱,建议统一使用新API
273+
274+
## 相关链接
275+
276+
- [企业微信会话存档官方文档](https://developer.work.weixin.qq.com/document/path/91360)
277+
- [WxJava GitHub 仓库](https://github.com/binarywang/WxJava)
278+
- [问题反馈](https://github.com/binarywang/WxJava/issues)
279+
280+
## 版本要求
281+
282+
- **最低版本**: 4.8.0
283+
- **推荐版本**: 最新版本
284+
285+
## 反馈与支持
286+
287+
如果在使用过程中遇到问题,请:
288+
289+
1. 查看本文档的常见问题部分
290+
2.GitHub 上提交 Issue
291+
3. 加入微信群获取社区支持
292+
293+
---
294+
295+
**最后更新时间**: 2026-01-14

0 commit comments

Comments
 (0)