mirror of
https://gitee.com/binary/weixin-java-tools.git
synced 2026-01-23 13:22:04 +08:00
🎨 #3848 【企业微信】修复会话存档SDK生命周期管理导致的JVM崩溃问题
This commit is contained in:
@@ -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(推荐使用)
|
||||
* 该方法不需要传入SDK,SDK由框架自动管理,更加安全
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 获取解密的聊天数据明文(推荐使用)
|
||||
* 该方法不需要传入SDK,SDK由框架自动管理,更加安全
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 获取媒体文件(推荐使用)
|
||||
* 该方法不需要传入SDK,SDK由框架自动管理,更加安全
|
||||
* 针对图片、文件等媒体数据,提供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;
|
||||
|
||||
/**
|
||||
* 获取媒体文件 传入一个lambda,each所有的数据分片byte[],更加灵活
|
||||
* 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。
|
||||
@@ -85,10 +152,29 @@ public interface WxCpMsgAuditService {
|
||||
* @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000
|
||||
* @param action 传入一个lambda,each所有的数据分片
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 获取媒体文件 传入一个lambda,each所有的数据分片byte[],更加灵活(推荐使用)
|
||||
* 该方法不需要传入SDK,SDK由框架自动管理,更加安全
|
||||
* 针对图片、文件等媒体数据,提供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 传入一个lambda,each所有的数据分片
|
||||
* @throws WxErrorException the wx error exception
|
||||
*/
|
||||
void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
|
||||
@NonNull Consumer<byte[]> action) throws WxErrorException;
|
||||
|
||||
/**
|
||||
* 获取会话内容存档开启成员列表
|
||||
* 企业可通过此接口,获取企业开启会话内容存档的成员列表
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user