From c849913f3ba713831d4f1ba19196aa1d25f48d39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:47:05 +0000 Subject: [PATCH 1/6] Initial plan From 54be19d4e3350ede211f4ec9fd1a5cc8fd1371d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:55:07 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=E6=B7=BB=E5=8A=A0SDK=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E8=AE=A1=E6=95=B0=E6=9C=BA=E5=88=B6=E5=92=8C=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E5=AE=89=E5=85=A8API=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../weixin/cp/api/WxCpMsgAuditService.java | 86 +++++++++++ .../cp/api/impl/WxCpMsgAuditServiceImpl.java | 135 ++++++++++++++++++ .../weixin/cp/config/WxCpConfigStorage.java | 26 ++++ .../cp/config/impl/WxCpDefaultConfigImpl.java | 30 ++++ .../cp/config/impl/WxCpRedisConfigImpl.java | 30 ++++ 5 files changed, 307 insertions(+) diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java index 221caf2e70..b754e32b7e 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java @@ -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 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 @@ WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatD * @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 @@ WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatD * @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接口拉取数据内容。 + *

+ * 注意: + * 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。 + * 详情可以看官方文档,亦可阅读此接口源码。 + * + * @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 @@ void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, St * @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 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 action) throws WxErrorException; + /** * 获取会话内容存档开启成员列表 * 企业可通过此接口,获取企业开启会话内容存档的成员列表 diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java index cdf559ad7a..5bfecd13bc 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java @@ -280,4 +280,139 @@ public WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeR return WxCpAgreeInfo.fromJson(responseContent); } + @Override + public List getChatRecords(long seq, @NonNull long limit, String proxy, String passwd, + @NonNull long timeout) throws Exception { + // 获取或初始化SDK + long sdk = this.initSdk(); + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + 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()); + } + + return chatDatas.getChatData(); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(sdk); + } + } + + @Override + public WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData, + @NonNull Integer pkcs1) throws Exception { + // 获取或初始化SDK + long sdk = this.initSdk(); + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + try { + String plainText = this.decryptChatData(sdk, chatData, pkcs1); + return WxCpChatModel.fromJson(plainText); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(sdk); + } + } + + @Override + public String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData, + @NonNull Integer pkcs1) throws Exception { + // 获取或初始化SDK + long sdk = this.initSdk(); + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + try { + return this.decryptChatData(sdk, chatData, pkcs1); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(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.initSdk(); + } catch (WxErrorException e) { + throw e; + } catch (Exception e) { + throw new WxErrorException(e); + } + + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + try { + File targetFile = new File(targetFilePath); + if (!targetFile.getParentFile().exists()) { + targetFile.getParentFile().mkdirs(); + } + this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> { + try { + // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。 + FileOutputStream outputStream = new FileOutputStream(targetFile, true); + outputStream.write(i); + outputStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(sdk); + } + } + + @Override + public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull Consumer action) throws WxErrorException { + // 获取或初始化SDK + long sdk; + try { + sdk = this.initSdk(); + } catch (WxErrorException e) { + throw e; + } catch (Exception e) { + throw new WxErrorException(e); + } + + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + try { + this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, action); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(sdk); + } + } + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java index 8b968e540c..f9c0e5d94f 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java @@ -292,4 +292,30 @@ public interface WxCpConfigStorage { * 使会话存档SDK过期 */ void expireMsgAuditSdk(); + + /** + * 增加会话存档SDK的引用计数 + * 用于支持多线程安全的SDK生命周期管理 + * + * @param sdk sdk id + * @return 增加后的引用计数 + */ + int incrementMsgAuditSdkRefCount(long sdk); + + /** + * 减少会话存档SDK的引用计数 + * 当引用计数降为0时,自动销毁SDK + * + * @param sdk sdk id + * @return 减少后的引用计数,如果返回0表示SDK已被销毁 + */ + int decrementMsgAuditSdkRefCount(long sdk); + + /** + * 获取会话存档SDK的引用计数 + * + * @param sdk sdk id + * @return 当前引用计数 + */ + int getMsgAuditSdkRefCount(long sdk); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java index 4bf13f24ea..cd2bdb45d0 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java @@ -54,6 +54,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; @@ -473,10 +477,36 @@ public synchronized void updateMsgAuditSdk(long sdk, int expiresInSeconds) { this.msgAuditSdk = sdk; // 预留200秒的时间 this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; + // 重置引用计数 + this.msgAuditSdkRefCount = 0; } @Override public void expireMsgAuditSdk() { this.msgAuditSdkExpiresTime = 0; } + + @Override + public synchronized int incrementMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk) { + return ++this.msgAuditSdkRefCount; + } + return 0; + } + + @Override + public synchronized int decrementMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) { + return --this.msgAuditSdkRefCount; + } + return 0; + } + + @Override + public synchronized int getMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk) { + return this.msgAuditSdkRefCount; + } + return 0; + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java index 49cd7c4559..93ec3d5677 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java @@ -55,6 +55,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. @@ -491,10 +495,36 @@ public synchronized void updateMsgAuditSdk(long sdk, int expiresInSeconds) { this.msgAuditSdk = sdk; // 预留200秒的时间 this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; + // 重置引用计数 + this.msgAuditSdkRefCount = 0; } @Override public void expireMsgAuditSdk() { this.msgAuditSdkExpiresTime = 0; } + + @Override + public synchronized int incrementMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk) { + return ++this.msgAuditSdkRefCount; + } + return 0; + } + + @Override + public synchronized int decrementMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) { + return --this.msgAuditSdkRefCount; + } + return 0; + } + + @Override + public synchronized int getMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk) { + return this.msgAuditSdkRefCount; + } + return 0; + } } From 54c51bd04eaed2107442e0ba7d8c752862d0ee28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:56:37 +0000 Subject: [PATCH 3/6] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0API=E7=9A=84?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../weixin/cp/api/WxCpMsgAuditTest.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java index ec7362ed5d..1d63216ca0 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java @@ -754,6 +754,76 @@ public void testGetMediaFile() throws Exception { Finance.DestroySdk(chatDatas.getSdk()); } + /** + * 测试新的安全API方法(推荐使用) + * 这些方法不需要手动管理SDK生命周期,更加安全 + */ + @Test + public void testNewSafeApi() throws Exception { + WxCpMsgAuditService msgAuditService = cpService.getMsgAuditService(); + + // 测试新的getChatRecords方法 - 不暴露SDK + List 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; + } + + // 测试新的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(); + } else { + file.delete(); + } + + // 使用新的API下载媒体文件 + msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath); + log.info("媒体文件下载成功:{}", targetPath); + } + } + + // 注意:使用新API无需手动调用 Finance.DestroySdk(),SDK由框架自动管理 + } + // 测试Uint64类型 public static void main(String[] args){ /* From 6e3039cd885f7016789404e5cb0fa4d04e7c108d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:59:33 +0000 Subject: [PATCH 4/6] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=AD=98=E6=A1=A3SDK=E5=AE=89=E5=85=A8=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md | 295 ++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md diff --git a/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md b/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md new file mode 100644 index 0000000000..b3a3ea1d33 --- /dev/null +++ b/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md @@ -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 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 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 From 7d637399dfa6ea8333c2830cfdbecc9f48520680 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 04:03:29 +0000 Subject: [PATCH 5/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E5=8F=8D=E9=A6=88=EF=BC=9A=E5=AE=8C=E5=96=84?= =?UTF-8?q?SDK=E8=87=AA=E5=8A=A8=E9=94=80=E6=AF=81=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=92=8C=E6=B5=8B=E8=AF=95=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../me/chanjar/weixin/cp/config/WxCpConfigStorage.java | 2 +- .../weixin/cp/config/impl/WxCpDefaultConfigImpl.java | 10 +++++++++- .../weixin/cp/config/impl/WxCpRedisConfigImpl.java | 10 +++++++++- .../me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java | 10 +++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java index f9c0e5d94f..f7729f4a3f 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java @@ -304,7 +304,7 @@ public interface WxCpConfigStorage { /** * 减少会话存档SDK的引用计数 - * 当引用计数降为0时,自动销毁SDK + * 当引用计数降为0时,自动销毁SDK以释放资源 * * @param sdk sdk id * @return 减少后的引用计数,如果返回0表示SDK已被销毁 diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java index cd2bdb45d0..a3f94be590 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java @@ -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; @@ -497,7 +498,14 @@ public synchronized int incrementMsgAuditSdkRefCount(long sdk) { @Override public synchronized int decrementMsgAuditSdkRefCount(long sdk) { if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) { - return --this.msgAuditSdkRefCount; + int newCount = --this.msgAuditSdkRefCount; + // 当引用计数降为0时,自动销毁SDK以释放资源 + if (newCount == 0) { + Finance.DestroySdk(sdk); + this.msgAuditSdk = 0; + this.msgAuditSdkExpiresTime = 0; + } + return newCount; } return 0; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java index 93ec3d5677..64c74e2650 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java @@ -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; @@ -515,7 +516,14 @@ public synchronized int incrementMsgAuditSdkRefCount(long sdk) { @Override public synchronized int decrementMsgAuditSdkRefCount(long sdk) { if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) { - return --this.msgAuditSdkRefCount; + int newCount = --this.msgAuditSdkRefCount; + // 当引用计数降为0时,自动销毁SDK以释放资源 + if (newCount == 0) { + Finance.DestroySdk(sdk); + this.msgAuditSdk = 0; + this.msgAuditSdkExpiresTime = 0; + } + return newCount; } return 0; } diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java index 1d63216ca0..a1ea40f3fb 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java @@ -803,15 +803,23 @@ public void testNewSafeApi() throws Exception { 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(); - } else { + } + + // 删除已存在的文件 + if (file.exists()) { file.delete(); } From 1d4e08cdea039310c4092af59b0a432f9abfb1d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:02:35 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=9A=E6=94=B9=E8=BF=9BSDK=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E8=AE=A1=E6=95=B0=E6=9C=BA=E5=88=B6=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=8E=9F=E5=AD=90=E6=93=8D=E4=BD=9C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E7=AB=9E=E6=80=81=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../cp/api/impl/WxCpMsgAuditServiceImpl.java | 109 ++++++++++-------- .../weixin/cp/config/WxCpConfigStorage.java | 23 +++- .../cp/config/impl/WxCpDefaultConfigImpl.java | 41 ++++++- .../cp/config/impl/WxCpRedisConfigImpl.java | 41 ++++++- 4 files changed, 153 insertions(+), 61 deletions(-) diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java index 5bfecd13bc..8e474a6980 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java @@ -137,6 +137,49 @@ private synchronized long initSdk() throws WxErrorException { 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 { @@ -283,12 +326,8 @@ public WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeR @Override public List getChatRecords(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception { - // 获取或初始化SDK - long sdk = this.initSdk(); - WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); - - // 增加引用计数 - configStorage.incrementMsgAuditSdkRefCount(sdk); + // 获取SDK并自动增加引用计数(原子操作) + long sdk = this.acquireSdk(); try { long slice = Finance.NewSlice(); @@ -308,66 +347,51 @@ public List getChatRecords(long seq, @NonNull long l return chatDatas.getChatData(); } finally { - // 减少引用计数 - configStorage.decrementMsgAuditSdkRefCount(sdk); + // 释放SDK引用计数(原子操作) + this.releaseSdk(sdk); } } @Override public WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception { - // 获取或初始化SDK - long sdk = this.initSdk(); - WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); - - // 增加引用计数 - configStorage.incrementMsgAuditSdkRefCount(sdk); + // 获取SDK并自动增加引用计数(原子操作) + long sdk = this.acquireSdk(); try { String plainText = this.decryptChatData(sdk, chatData, pkcs1); return WxCpChatModel.fromJson(plainText); } finally { - // 减少引用计数 - configStorage.decrementMsgAuditSdkRefCount(sdk); + // 释放SDK引用计数(原子操作) + this.releaseSdk(sdk); } } @Override public String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception { - // 获取或初始化SDK - long sdk = this.initSdk(); - WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); - - // 增加引用计数 - configStorage.incrementMsgAuditSdkRefCount(sdk); + // 获取SDK并自动增加引用计数(原子操作) + long sdk = this.acquireSdk(); try { return this.decryptChatData(sdk, chatData, pkcs1); } finally { - // 减少引用计数 - configStorage.decrementMsgAuditSdkRefCount(sdk); + // 释放SDK引用计数(原子操作) + this.releaseSdk(sdk); } } @Override public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull String targetFilePath) throws WxErrorException { - // 获取或初始化SDK + // 获取SDK并自动增加引用计数(原子操作) long sdk; try { - sdk = this.initSdk(); - } catch (WxErrorException e) { - throw e; + sdk = this.acquireSdk(); } catch (Exception e) { throw new WxErrorException(e); } - WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); - - // 增加引用计数 - configStorage.incrementMsgAuditSdkRefCount(sdk); - try { File targetFile = new File(targetFilePath); if (!targetFile.getParentFile().exists()) { @@ -384,34 +408,27 @@ public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String pa } }); } finally { - // 减少引用计数 - configStorage.decrementMsgAuditSdkRefCount(sdk); + // 释放SDK引用计数(原子操作) + this.releaseSdk(sdk); } } @Override public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull Consumer action) throws WxErrorException { - // 获取或初始化SDK + // 获取SDK并自动增加引用计数(原子操作) long sdk; try { - sdk = this.initSdk(); - } catch (WxErrorException e) { - throw e; + sdk = this.acquireSdk(); } catch (Exception e) { throw new WxErrorException(e); } - WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); - - // 增加引用计数 - configStorage.incrementMsgAuditSdkRefCount(sdk); - try { this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, action); } finally { - // 减少引用计数 - configStorage.decrementMsgAuditSdkRefCount(sdk); + // 释放SDK引用计数(原子操作) + this.releaseSdk(sdk); } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java index f7729f4a3f..fd96d76c30 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java @@ -298,7 +298,7 @@ public interface WxCpConfigStorage { * 用于支持多线程安全的SDK生命周期管理 * * @param sdk sdk id - * @return 增加后的引用计数 + * @return 增加后的引用计数,如果SDK不匹配返回-1 */ int incrementMsgAuditSdkRefCount(long sdk); @@ -307,7 +307,7 @@ public interface WxCpConfigStorage { * 当引用计数降为0时,自动销毁SDK以释放资源 * * @param sdk sdk id - * @return 减少后的引用计数,如果返回0表示SDK已被销毁 + * @return 减少后的引用计数,如果返回0表示SDK已被销毁,如果SDK不匹配返回-1 */ int decrementMsgAuditSdkRefCount(long sdk); @@ -315,7 +315,24 @@ public interface WxCpConfigStorage { * 获取会话存档SDK的引用计数 * * @param sdk sdk id - * @return 当前引用计数 + * @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); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java index a3f94be590..94753f4189 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java @@ -475,6 +475,10 @@ public boolean isMsgAuditSdkExpired() { @Override public synchronized void updateMsgAuditSdk(long sdk, int expiresInSeconds) { + // 如果有旧的SDK且引用计数为0,先销毁旧的SDK + if (this.msgAuditSdk > 0 && this.msgAuditSdk != sdk && this.msgAuditSdkRefCount == 0) { + Finance.DestroySdk(this.msgAuditSdk); + } this.msgAuditSdk = sdk; // 预留200秒的时间 this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; @@ -489,10 +493,10 @@ public void expireMsgAuditSdk() { @Override public synchronized int incrementMsgAuditSdkRefCount(long sdk) { - if (this.msgAuditSdk == sdk) { + if (this.msgAuditSdk == sdk && sdk > 0) { return ++this.msgAuditSdkRefCount; } - return 0; + return -1; // SDK不匹配,返回-1表示错误 } @Override @@ -500,21 +504,46 @@ public synchronized int decrementMsgAuditSdkRefCount(long sdk) { if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) { int newCount = --this.msgAuditSdkRefCount; // 当引用计数降为0时,自动销毁SDK以释放资源 - if (newCount == 0) { + // 再次检查SDK是否仍然是当前缓存的SDK(防止并发重新初始化) + if (newCount == 0 && this.msgAuditSdk == sdk) { Finance.DestroySdk(sdk); this.msgAuditSdk = 0; this.msgAuditSdkExpiresTime = 0; } return newCount; } - return 0; + return -1; // SDK不匹配或引用计数已为0,返回-1表示错误 } @Override public synchronized int getMsgAuditSdkRefCount(long sdk) { - if (this.msgAuditSdk == sdk) { + if (this.msgAuditSdk == sdk && sdk > 0) { return this.msgAuditSdkRefCount; } - return 0; + 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; + } + } } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java index 64c74e2650..28203956fc 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java @@ -493,6 +493,10 @@ public boolean isMsgAuditSdkExpired() { @Override public synchronized void updateMsgAuditSdk(long sdk, int expiresInSeconds) { + // 如果有旧的SDK且引用计数为0,先销毁旧的SDK + if (this.msgAuditSdk > 0 && this.msgAuditSdk != sdk && this.msgAuditSdkRefCount == 0) { + Finance.DestroySdk(this.msgAuditSdk); + } this.msgAuditSdk = sdk; // 预留200秒的时间 this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; @@ -507,10 +511,10 @@ public void expireMsgAuditSdk() { @Override public synchronized int incrementMsgAuditSdkRefCount(long sdk) { - if (this.msgAuditSdk == sdk) { + if (this.msgAuditSdk == sdk && sdk > 0) { return ++this.msgAuditSdkRefCount; } - return 0; + return -1; // SDK不匹配,返回-1表示错误 } @Override @@ -518,21 +522,46 @@ public synchronized int decrementMsgAuditSdkRefCount(long sdk) { if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) { int newCount = --this.msgAuditSdkRefCount; // 当引用计数降为0时,自动销毁SDK以释放资源 - if (newCount == 0) { + // 再次检查SDK是否仍然是当前缓存的SDK(防止并发重新初始化) + if (newCount == 0 && this.msgAuditSdk == sdk) { Finance.DestroySdk(sdk); this.msgAuditSdk = 0; this.msgAuditSdkExpiresTime = 0; } return newCount; } - return 0; + return -1; // SDK不匹配或引用计数已为0,返回-1表示错误 } @Override public synchronized int getMsgAuditSdkRefCount(long sdk) { - if (this.msgAuditSdk == sdk) { + if (this.msgAuditSdk == sdk && sdk > 0) { return this.msgAuditSdkRefCount; } - return 0; + 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; + } + } } }