🎨 #3935【企业微信】重构会话存档SDK生命周期为ThreadLocal模式
This commit is contained in:
@@ -215,4 +215,20 @@ public interface WxCpMsgAuditService {
|
||||
*/
|
||||
WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException;
|
||||
|
||||
/**
|
||||
* 关闭当前线程持有的SDK,释放本地资源。
|
||||
* <p>
|
||||
* 在线程池场景下,任务结束后必须在 finally 块中调用此方法,防止SDK实例随线程复用而泄漏。
|
||||
* 独立线程或一次性任务也建议调用,以主动释放原生资源。
|
||||
*/
|
||||
void closeThreadLocalSdk();
|
||||
|
||||
/**
|
||||
* 关闭所有会话存档SDK实例,释放全部原生资源。
|
||||
* <p>
|
||||
* 适用于应用关闭阶段(如 Spring Bean 销毁阶段 {@code @PreDestroy} 或 Shutdown Hook)。
|
||||
* 调用后,所有线程的SDK均不可再使用。
|
||||
*/
|
||||
void closeAllSdks();
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.MsgAudit.*;
|
||||
@@ -37,16 +39,17 @@ import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.MsgAudit.*;
|
||||
public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
|
||||
private final WxCpService cpService;
|
||||
|
||||
/**
|
||||
* SDK初始化有效期,根据企微文档为7200秒
|
||||
*/
|
||||
private static final int SDK_EXPIRES_TIME = 7200;
|
||||
/** 每个线程持有独立 SDK 实例,懒初始化,线程内跨调用复用 */
|
||||
private final ThreadLocal<Long> threadLocalSdk = new ThreadLocal<>();
|
||||
|
||||
/** 跟踪所有已创建的 SDK,用于 closeAllSdks() 统一清理 */
|
||||
private final Set<Long> managedSdks = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@Override
|
||||
public WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd,
|
||||
@NonNull long timeout) throws Exception {
|
||||
// 获取或初始化SDK
|
||||
long sdk = this.initSdk();
|
||||
// 旧版 API:每次调用创建新 SDK,由调用方负责通过 Finance.DestroySdk(chatDatas.getSdk()) 释放
|
||||
long sdk = this.createSdk();
|
||||
|
||||
long slice = Finance.NewSlice();
|
||||
long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
|
||||
@@ -68,23 +71,39 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或初始化SDK,如果SDK已过期则重新初始化
|
||||
* 获取当前线程的 SDK,不存在则初始化。
|
||||
* SDK 在线程内跨调用复用,无需每次重新初始化。
|
||||
*
|
||||
* @return sdk id
|
||||
* @throws WxErrorException 初始化失败时抛出异常
|
||||
*/
|
||||
private synchronized long initSdk() throws WxErrorException {
|
||||
private long getOrInitThreadLocalSdk() throws WxErrorException {
|
||||
Long sdk = threadLocalSdk.get();
|
||||
if (sdk != null && sdk > 0) {
|
||||
// 校验句柄是否仍受管理:closeAllSdks() 后其他线程 ThreadLocal 可能保留已销毁的 id
|
||||
if (managedSdks.contains(sdk)) {
|
||||
return sdk;
|
||||
}
|
||||
log.warn("线程 [{}] 发现已失效的会话存档SDK句柄 sdk={},请检查调用逻辑", Thread.currentThread().getName(), sdk);
|
||||
threadLocalSdk.remove();
|
||||
throw new WxErrorException("线程 [" + Thread.currentThread().getName() + "] 获取会话存档SDK失败,请检查是否已调用 closeAllSdks()");
|
||||
}
|
||||
long newSdk = createSdk();
|
||||
threadLocalSdk.set(newSdk);
|
||||
managedSdks.add(newSdk);
|
||||
log.info("线程 [{}] 初始化会话存档SDK成功,sdk={}", Thread.currentThread().getName(), newSdk);
|
||||
return newSdk;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并初始化一个新的会话存档 SDK 实例。
|
||||
* <p>通常通过 {@link #getOrInitThreadLocalSdk()} 间接调用以复用 ThreadLocal 中的实例;
|
||||
* 旧版直接暴露 sdk 的 API(如 {@link #getChatDatas})也会直接调用本方法,此时 SDK 由调用方自行管理。</p>
|
||||
* <p>Finance.loadingLibraries() 底层依赖 System.load(),JVM 保证同一库不重复加载,多线程并发调用安全。</p>
|
||||
*/
|
||||
private long createSdk() throws WxErrorException {
|
||||
WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage();
|
||||
|
||||
// 检查SDK是否已缓存且未过期
|
||||
if (!configStorage.isMsgAuditSdkExpired()) {
|
||||
long cachedSdk = configStorage.getMsgAuditSdk();
|
||||
if (cachedSdk > 0) {
|
||||
return cachedSdk;
|
||||
}
|
||||
}
|
||||
|
||||
// SDK未初始化或已过期,需要重新初始化
|
||||
String configPath = configStorage.getMsgAuditLibPath();
|
||||
if (StringUtils.isEmpty(configPath)) {
|
||||
throw new WxErrorException("请配置会话存档sdk文件的路径,不要配错了!!");
|
||||
@@ -130,55 +149,31 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
|
||||
Finance.DestroySdk(sdk);
|
||||
throw new WxErrorException("init sdk err ret " + ret);
|
||||
}
|
||||
|
||||
// 缓存SDK
|
||||
configStorage.updateMsgAuditSdk(sdk, SDK_EXPIRES_TIME);
|
||||
log.debug("初始化会话存档SDK成功,sdk={}", sdk);
|
||||
|
||||
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;
|
||||
@Override
|
||||
public void closeThreadLocalSdk() {
|
||||
Long sdk = threadLocalSdk.get();
|
||||
// 先从 managedSdks 摘除,摘除成功才调 DestroySdk,防止与 closeAllSdks() 并发时 double-free
|
||||
if (sdk != null && managedSdks.remove(sdk)) {
|
||||
Finance.DestroySdk(sdk);
|
||||
log.info("线程 [{}] 关闭会话存档SDK,sdk={}", Thread.currentThread().getName(), sdk);
|
||||
}
|
||||
|
||||
// SDK未初始化或已过期,需要初始化
|
||||
// initSdk()方法已经是synchronized的,确保只有一个线程初始化
|
||||
sdk = this.initSdk();
|
||||
|
||||
// 初始化后增加引用计数
|
||||
int refCount = configStorage.incrementMsgAuditSdkRefCount(sdk);
|
||||
if (refCount < 0) {
|
||||
// SDK已经被替换,需要重新获取
|
||||
return acquireSdk();
|
||||
}
|
||||
|
||||
return sdk;
|
||||
threadLocalSdk.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放SDK引用计数
|
||||
*
|
||||
* @param sdk sdk id
|
||||
*/
|
||||
private void releaseSdk(long sdk) {
|
||||
if (sdk > 0) {
|
||||
cpService.getWxCpConfigStorage().releaseMsgAuditSdk(sdk);
|
||||
@Override
|
||||
public void closeAllSdks() {
|
||||
// 逐一 remove 后再 Destroy,防止与 closeThreadLocalSdk() 并发时 double-free
|
||||
Long[] sdks = managedSdks.toArray(new Long[0]);
|
||||
for (Long sdk : sdks) {
|
||||
if (managedSdks.remove(sdk)) {
|
||||
Finance.DestroySdk(sdk);
|
||||
log.info("关闭会话存档SDK,sdk={}", sdk);
|
||||
}
|
||||
}
|
||||
threadLocalSdk.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -240,17 +235,18 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
|
||||
* 为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。
|
||||
*/
|
||||
File targetFile = new File(targetFilePath);
|
||||
if (!targetFile.getParentFile().exists()) {
|
||||
targetFile.getParentFile().mkdirs();
|
||||
File parentDir = targetFile.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs();
|
||||
}
|
||||
this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> {
|
||||
try {
|
||||
// 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
|
||||
FileOutputStream outputStream = new FileOutputStream(targetFile, true);
|
||||
outputStream.write(i);
|
||||
outputStream.close();
|
||||
try (FileOutputStream outputStream = new FileOutputStream(targetFile, true)) {
|
||||
outputStream.write(i);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("写入媒体文件分片失败,targetFilePath={}", targetFilePath, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -280,7 +276,7 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
|
||||
// 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
|
||||
action.accept(Finance.GetData(mediaData));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("处理媒体文件分片失败,sdkfileid={}", sdkfileid, e);
|
||||
}
|
||||
|
||||
if (Finance.IsMediaDataFinish(mediaData) == 1) {
|
||||
@@ -327,69 +323,48 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
|
||||
@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();
|
||||
long sdk = this.getOrInitThreadLocalSdk();
|
||||
|
||||
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);
|
||||
long slice = Finance.NewSlice();
|
||||
long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
|
||||
if (ret != 0) {
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
long sdk = this.getOrInitThreadLocalSdk();
|
||||
String plainText = this.decryptChatData(sdk, chatData, pkcs1);
|
||||
return WxCpChatModel.fromJson(plainText);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
long sdk = this.getOrInitThreadLocalSdk();
|
||||
return this.decryptChatData(sdk, chatData, pkcs1);
|
||||
}
|
||||
|
||||
@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();
|
||||
sdk = this.getOrInitThreadLocalSdk();
|
||||
} catch (Exception e) {
|
||||
throw new WxErrorException(e);
|
||||
}
|
||||
@@ -397,54 +372,43 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
|
||||
// 使用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();
|
||||
File targetFile = new File(targetFilePath);
|
||||
File parentDir = targetFile.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs();
|
||||
}
|
||||
this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> {
|
||||
// 如果之前已经发生异常,不再继续处理
|
||||
if (exceptionHolder.get() != null) {
|
||||
return;
|
||||
}
|
||||
this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> {
|
||||
// 如果之前已经发生异常,不再继续处理
|
||||
if (exceptionHolder.get() != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
|
||||
FileOutputStream outputStream = new FileOutputStream(targetFile, true);
|
||||
try {
|
||||
// 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
|
||||
try (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);
|
||||
} catch (Exception e) {
|
||||
exceptionHolder.set(e);
|
||||
}
|
||||
} finally {
|
||||
// 释放SDK引用计数(原子操作)
|
||||
this.releaseSdk(sdk);
|
||||
});
|
||||
|
||||
// 检查是否发生异常,如果有则抛出
|
||||
Exception caughtException = exceptionHolder.get();
|
||||
if (caughtException != null) {
|
||||
throw new WxErrorException(caughtException);
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
sdk = this.getOrInitThreadLocalSdk();
|
||||
} catch (Exception e) {
|
||||
throw new WxErrorException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, action);
|
||||
} finally {
|
||||
// 释放SDK引用计数(原子操作)
|
||||
this.releaseSdk(sdk);
|
||||
}
|
||||
this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, action);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -300,18 +300,24 @@ public interface WxCpConfigStorage {
|
||||
void updateMsgAuditAccessToken(String accessToken, int expiresInSeconds);
|
||||
|
||||
/**
|
||||
* 获取会话存档SDK
|
||||
* 会话存档SDK初始化后有效期为7200秒,无需每次重新初始化
|
||||
* 获取会话存档SDK(历史接口)。
|
||||
* <p>历史实现中,会话存档 SDK 初始化后有效期为 7200 秒,由 ConfigStorage 负责维护;
|
||||
* 该语义现已废弃,不再保证。</p>
|
||||
*
|
||||
* @return sdk id,如果未初始化或已过期返回0
|
||||
* @return sdk id;历史实现中如果未初始化或已过期返<EFBFBD><EFBFBD> 0,当前实现仅为兼容旧代码保留此方法
|
||||
* @deprecated SDK 生命周期已改由 {@link me.chanjar.weixin.cp.api.WxCpMsgAuditService} 内部的 ThreadLocal
|
||||
* 模式管理,不再依赖 ConfigStorage 缓存。请迁移至新接口。
|
||||
*/
|
||||
@Deprecated
|
||||
long getMsgAuditSdk();
|
||||
|
||||
/**
|
||||
* 检查会话存档SDK是否已过期
|
||||
*
|
||||
* @return true: 已过期, false: 未过期
|
||||
* @deprecated SDK 生命周期已改由 ThreadLocal 模式管理,过期检查不再必要。
|
||||
*/
|
||||
@Deprecated
|
||||
boolean isMsgAuditSdkExpired();
|
||||
|
||||
/**
|
||||
@@ -319,12 +325,17 @@ public interface WxCpConfigStorage {
|
||||
*
|
||||
* @param sdk sdk id
|
||||
* @param expiresInSeconds 过期时间(秒)
|
||||
* @deprecated SDK 生命周期已改由 ThreadLocal 模式管理,无需通过 ConfigStorage 更新。
|
||||
*/
|
||||
@Deprecated
|
||||
void updateMsgAuditSdk(long sdk, int expiresInSeconds);
|
||||
|
||||
/**
|
||||
* 使会话存档SDK过期
|
||||
*
|
||||
* @deprecated SDK 生命周期已改由 ThreadLocal 模式管理,此方法已无实际作用。
|
||||
*/
|
||||
@Deprecated
|
||||
void expireMsgAuditSdk();
|
||||
|
||||
/**
|
||||
@@ -333,7 +344,9 @@ public interface WxCpConfigStorage {
|
||||
*
|
||||
* @param sdk sdk id
|
||||
* @return 增加后的引用计数,如果SDK不匹配返回-1
|
||||
* @deprecated 引用计数机制已废弃,由 ThreadLocal 模式替代。
|
||||
*/
|
||||
@Deprecated
|
||||
int incrementMsgAuditSdkRefCount(long sdk);
|
||||
|
||||
/**
|
||||
@@ -342,7 +355,9 @@ public interface WxCpConfigStorage {
|
||||
*
|
||||
* @param sdk sdk id
|
||||
* @return 减少后的引用计数,如果返回0表示SDK已被销毁,如果SDK不匹配返回-1
|
||||
* @deprecated 引用计数机制已废弃,由 ThreadLocal 模式替代。
|
||||
*/
|
||||
@Deprecated
|
||||
int decrementMsgAuditSdkRefCount(long sdk);
|
||||
|
||||
/**
|
||||
@@ -350,7 +365,9 @@ public interface WxCpConfigStorage {
|
||||
*
|
||||
* @param sdk sdk id
|
||||
* @return 当前引用计数,如果SDK不匹配返回-1
|
||||
* @deprecated 引用计数机制已废弃,由 ThreadLocal 模式替代。
|
||||
*/
|
||||
@Deprecated
|
||||
int getMsgAuditSdkRefCount(long sdk);
|
||||
|
||||
/**
|
||||
@@ -359,7 +376,9 @@ public interface WxCpConfigStorage {
|
||||
* 此方法用于在获取SDK后立即增加引用计数,避免并发问题
|
||||
*
|
||||
* @return 当前有效的SDK id并已增加引用计数,如果SDK无效返回0
|
||||
* @deprecated 引用计数机制已废弃,由 ThreadLocal 模式替代。
|
||||
*/
|
||||
@Deprecated
|
||||
long acquireMsgAuditSdk();
|
||||
|
||||
/**
|
||||
@@ -367,6 +386,8 @@ public interface WxCpConfigStorage {
|
||||
* 此方法确保引用计数递减和SDK检查在同一个同步块内完成
|
||||
*
|
||||
* @param sdk sdk id
|
||||
* @deprecated 引用计数机制已废弃,由 ThreadLocal 模式替代。
|
||||
*/
|
||||
@Deprecated
|
||||
void releaseMsgAuditSdk(long sdk);
|
||||
}
|
||||
|
||||
@@ -61,12 +61,21 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
|
||||
protected transient Lock msgAuditAccessTokenLock = new ReentrantLock();
|
||||
/**
|
||||
* 会话存档SDK及其过期时间
|
||||
*
|
||||
* @deprecated SDK 生命周期已改由 {@link me.chanjar.weixin.cp.api.impl.WxCpMsgAuditServiceImpl} 内部的
|
||||
* ThreadLocal 模式管理,此字段保留仅为向后兼容。
|
||||
*/
|
||||
@Deprecated
|
||||
private volatile long msgAuditSdk;
|
||||
/** @deprecated 同 msgAuditSdk */
|
||||
@Deprecated
|
||||
private volatile long msgAuditSdkExpiresTime;
|
||||
/**
|
||||
* 会话存档SDK引用计数,用于多线程安全的生命周期管理
|
||||
* 会话存档SDK引用计数
|
||||
*
|
||||
* @deprecated 引用计数机制已废弃,由 ThreadLocal 模式替代。
|
||||
*/
|
||||
@Deprecated
|
||||
private volatile int msgAuditSdkRefCount;
|
||||
private volatile String oauth2redirectUri;
|
||||
private volatile String httpProxyHost;
|
||||
@@ -500,16 +509,19 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public long getMsgAuditSdk() {
|
||||
return this.msgAuditSdk;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public boolean isMsgAuditSdkExpired() {
|
||||
return System.currentTimeMillis() > this.msgAuditSdkExpiresTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public synchronized void updateMsgAuditSdk(long sdk, int expiresInSeconds) {
|
||||
// 如果有旧的SDK且不同于新的SDK,需要销毁旧的SDK
|
||||
if (this.msgAuditSdk > 0 && this.msgAuditSdk != sdk) {
|
||||
@@ -525,11 +537,13 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void expireMsgAuditSdk() {
|
||||
this.msgAuditSdkExpiresTime = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public synchronized int incrementMsgAuditSdkRefCount(long sdk) {
|
||||
if (this.msgAuditSdk == sdk && sdk > 0) {
|
||||
return ++this.msgAuditSdkRefCount;
|
||||
@@ -538,6 +552,7 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public synchronized int decrementMsgAuditSdkRefCount(long sdk) {
|
||||
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
|
||||
int newCount = --this.msgAuditSdkRefCount;
|
||||
@@ -554,6 +569,7 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public synchronized int getMsgAuditSdkRefCount(long sdk) {
|
||||
if (this.msgAuditSdk == sdk && sdk > 0) {
|
||||
return this.msgAuditSdkRefCount;
|
||||
@@ -562,6 +578,7 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public synchronized long acquireMsgAuditSdk() {
|
||||
// 检查SDK是否有效(已初始化且未过期)
|
||||
if (this.msgAuditSdk > 0 && !isMsgAuditSdkExpired()) {
|
||||
@@ -572,6 +589,7 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public synchronized void releaseMsgAuditSdk(long sdk) {
|
||||
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
|
||||
int newCount = --this.msgAuditSdkRefCount;
|
||||
|
||||
@@ -756,80 +756,79 @@ public class WxCpMsgAuditTest {
|
||||
|
||||
/**
|
||||
* 测试新的安全API方法(推荐使用)
|
||||
* 这些方法不需要手动管理SDK生命周期,更加安全
|
||||
* 这些方法不需要手动管理SDK生命周期,SDK由框架 ThreadLocal 模式统一管理。
|
||||
* Finance.DestroySdk() 不会随线程结束自动执行,无论线程池还是独立线程,
|
||||
* 均应在 finally 块中调用 closeThreadLocalSdk() 以释放 native 资源。
|
||||
*/
|
||||
@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;
|
||||
|
||||
try {
|
||||
// 测试新的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();
|
||||
}
|
||||
|
||||
msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath);
|
||||
log.info("媒体文件下载成功:{}", targetPath);
|
||||
}
|
||||
|
||||
// 测试新的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);
|
||||
}
|
||||
} finally {
|
||||
// 必须显式调用:Finance.DestroySdk() 不会自动执行,不调用将导致 native 资源泄漏
|
||||
msgAuditService.closeThreadLocalSdk();
|
||||
}
|
||||
|
||||
// 注意:使用新API无需手动调用 Finance.DestroySdk(),SDK由框架自动管理
|
||||
}
|
||||
|
||||
// 测试Uint64类型
|
||||
|
||||
Reference in New Issue
Block a user