docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md
当前实现(4.8.x)通过"共享SDK + 引用计数 + 7200秒过期"来管理会话存档SDK生命周期。 该方案存在以下核心问题:
releaseSdk() 后引用计数归零即销毁SDK。对于"拉取→解密→下载媒体"这类典型串行调用链,每步操作都会触发重新初始化。核心原则:每个线程拥有独立SDK实例,懒初始化,生命周期与线程绑定。
ThreadLocal<Long> 为每个线程持有独立SDKcloseThreadLocalSdk()(线程结束时调)、closeAllSdks()(应用关闭时调)Thread A: init SDK_A → getChatRecords → getDecryptChatData → downloadMediaFile → [任务结束后调closeThreadLocalSdk]
Thread B: init SDK_B → getChatRecords → getDecryptChatData → downloadMediaFile → ...
Thread C: init SDK_C → ...
| 文件 | 变更类型 |
|---|---|
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java | 主要重构 |
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java | 新增接口方法 |
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java | 废弃旧SDK管理方法 |
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java | 废弃旧字段/方法 |
weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java | 补充测试 |
docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md | 更新文档 |
新增字段:
/** 每个线程持有独立SDK实例 */
private final ThreadLocal<Long> threadLocalSdk = new ThreadLocal<>();
/** 跟踪所有已创建SDK,用于统一清理 */
private final Set<Long> managedSdks = ConcurrentHashMap.newKeySet();
废弃字段/方法:
SDK_EXPIRES_TIME = 7200(无官方依据)initSdk()(由 getOrInitThreadLocalSdk() 替代)acquireSdk() / releaseSdk()(由ThreadLocal模式替代)新增核心方法:
/**
* 获取当前线程的SDK,不存在则创建。SDK在线程内跨调用复用,无需每次重新初始化。
*/
private long getOrInitThreadLocalSdk() throws WxErrorException {
Long sdk = threadLocalSdk.get();
if (sdk != null && sdk > 0) {
return sdk;
}
long newSdk = createSdk();
threadLocalSdk.set(newSdk);
managedSdks.add(newSdk);
log.info("线程 [{}] 初始化会话存档SDK成功,sdk={}", Thread.currentThread().getName(), newSdk);
return newSdk;
}
/**
* 创建并初始化一个新SDK(私有,只在当前线程无SDK时调用)
*/
private long createSdk() throws WxErrorException {
WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage();
// ... 与现有 initSdk() 内的库加载+Finance.NewSdk()+Finance.Init() 逻辑一致 ...
// 注意:Finance.loadingLibraries() 是幂等的(System.load内部防重复),多线程调用安全
}
/**
* 关闭当前线程持有的SDK,释放本地资源。
* 在线程任务结束时调用(如定时任务finally块,或线程池线程销毁时)。
*/
public void closeThreadLocalSdk() {
Long sdk = threadLocalSdk.get();
if (sdk != null && sdk > 0) {
Finance.DestroySdk(sdk);
managedSdks.remove(sdk);
threadLocalSdk.remove();
log.info("线程 [{}] 关闭会话存档SDK,sdk={}", Thread.currentThread().getName(), sdk);
}
}
/**
* 关闭所有线程持有的SDK。应用关闭时调用(如Spring @PreDestroy / Shutdown Hook)。
*/
public void closeAllSdks() {
managedSdks.forEach(sdk -> {
Finance.DestroySdk(sdk);
log.info("关闭会话存档SDK,sdk={}", sdk);
});
managedSdks.clear();
threadLocalSdk.remove();
}
更新新API方法(getChatRecords / getDecryptChatData / getChatRecordPlainText / downloadMediaFile):
getOrInitThreadLocalSdk() 替代 acquireSdk()releaseSdk(sdk) 调用(SDK不再每次释放)保留旧API方法不变(getChatDatas / getDecryptData / getChatPlainText / getMediaFile):
getOrInitThreadLocalSdk() 以保持一致性(旧方法也受益于ThreadLocal)initSdk() 的依赖/**
* 关闭当前线程持有的SDK,释放native资源。
* Finance.DestroySdk() 不会随线程结束自动执行,无论线程池还是独立线程,
* 均应在任务结束的finally块中调用本方法,防止native内存、连接等资源泄漏。
*/
void closeThreadLocalSdk();
/**
* 关闭所有会话存档SDK实例。
* 适用于应用关闭时(如Spring Bean销毁阶段)统一释放资源。
*/
void closeAllSdks();
对以下方法标记 @Deprecated(保留实现,不做破坏性删除):
getMsgAuditSdk() / updateMsgAuditSdk() / expireMsgAuditSdk() / isMsgAuditSdkExpired()acquireMsgAuditSdk() / releaseMsgAuditSdk()incrementMsgAuditSdkRefCount() / decrementMsgAuditSdkRefCount() / getMsgAuditSdkRefCount()msgAuditSdk、msgAuditSdkExpiresTime、msgAuditSdkRefCount 字段标记 @Deprecated@Deprecated// ✅ 典型用法(一次任务中串行调用,SDK在同线程内复用,无重复初始化)
WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService();
try {
List<WxCpChatDatas.WxCpChatData> records = msgAuditService.getChatRecords(seq, 100L, null, null, 30L);
for (WxCpChatDatas.WxCpChatData record : records) {
WxCpChatModel model = msgAuditService.getDecryptChatData(record, 2);
if ("image".equals(model.getMsgType())) {
msgAuditService.downloadMediaFile(model.getImage().getSdkFileId(), null, null, 30L, "/tmp/img.jpg");
}
}
} finally {
// 无论线程池还是独立线程,均建议在 finally 中显式调用。
// Finance.DestroySdk() 不会随线程结束自动执行,依赖 closeAllSdks() 兜底会造成
// native 内存/连接资源的延迟泄漏,对定时任务等长期运行场景尤其有害。
msgAuditService.closeThreadLocalSdk();
}
// 应用关闭时(Spring @PreDestroy 或 Shutdown Hook)
// msgAuditService.closeAllSdks();
closeThreadLocalSdk():线程池中线程会被复用,如不主动清理,下次任务仍会使用旧线程的SDK。对于计划任务/批处理,建议在 finally 块中调用。Finance.DestroySdk() 是 native 调用,不会随线程结束自动执行,JVM GC 也不会触发它。依赖 closeAllSdks() 兜底意味着 native 内存、网络连接等资源在整个应用运行期间一直持有,对定时任务等高频场景会持续积累,建议统一在 finally 块中调用 closeThreadLocalSdk()。threadLocalSdk 是实例字段(非static),不同 WxCpMsgAuditServiceImpl 实例(不同企业)的ThreadLocal独立,互不影响。Finance.loadingLibraries() 底层调用 System.load(),JVM保证同一库不重复加载,多线程并发调用安全。WxCpMsgAuditTest 中添加测试,验证同线程多次调用不触发重新初始化(可通过日志或mock Finance验证)getChatRecords + getDecryptChatData,观察无JVM崩溃closeThreadLocalSdk() 后下次任务能正确重新初始化SDKcloseAllSdks(),验证所有线程的SDK被正确销毁