Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,11 @@ public interface WxCpConfigStorage {

/**
* 减少会话存档SDK的引用计数
* 当引用计数降为0时,自动销毁SDK以释放资源
* 当引用计数降为0且SDK已过期时,才自动销毁SDK以释放资源
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本次语义变更后,仓库里其它外部文档/示例中“引用计数归零即释放 SDK”的表述(例如 docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md 的实现原理段落)可能会与这里不一致。建议检查并同步相关文档,避免用户误解 SDK 生命周期行为。

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

* 如果SDK尚未过期,保留SDK缓存以供后续调用复用
*
* @param sdk sdk id
* @return 减少后的引用计数,如果返回0表示SDK已被销毁,如果SDK不匹配返回-1
* @return 减少后的引用计数,如果SDK不匹配返回-1
*/
int decrementMsgAuditSdkRefCount(long sdk);

Expand All @@ -364,7 +365,8 @@ public interface WxCpConfigStorage {

/**
* 减少SDK引用计数并在必要时释放(原子操作)
* 此方法确保引用计数递减和SDK检查在同一个同步块内完成
* 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
* 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
*
* @param sdk sdk id
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,9 +541,9 @@ public synchronized int incrementMsgAuditSdkRefCount(long sdk) {
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) {
// 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
// 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
if (newCount == 0 && this.msgAuditSdk == sdk && isMsgAuditSdkExpired()) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
Expand Down Expand Up @@ -575,9 +575,9 @@ public synchronized long acquireMsgAuditSdk() {
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) {
// 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
// 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
if (newCount == 0 && this.msgAuditSdk == sdk && isMsgAuditSdkExpired()) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,9 +559,9 @@ public synchronized int incrementMsgAuditSdkRefCount(long sdk) {
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) {
// 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
// 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
if (newCount == 0 && this.msgAuditSdk == sdk && isMsgAuditSdkExpired()) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
Expand Down Expand Up @@ -593,9 +593,9 @@ public synchronized long acquireMsgAuditSdk() {
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) {
// 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
// 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
if (newCount == 0 && this.msgAuditSdk == sdk && isMsgAuditSdkExpired()) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package me.chanjar.weixin.cp.config.impl;

import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.lang.reflect.Field;

/**
* 测试 WxCpDefaultConfigImpl 中会话存档 SDK 引用计数的正确性
* 验证修复:SDK 在引用计数降为 0 但尚未过期时,不应被销毁
*
* @author GitHub Copilot
*/
public class WxCpDefaultConfigImplMsgAuditSdkTest {

/**
* 用于测试的未过期时间偏移量(毫秒),模拟 SDK 有效状态
*/
private static final long VALID_EXPIRATION_TIME_OFFSET = 7_000_000L;

private WxCpDefaultConfigImpl config;

@BeforeMethod
public void setUp() {
config = new WxCpDefaultConfigImpl();
}

/**
* 通过反射设置内部字段
*/
private void setField(String fieldName, Object value) throws Exception {
Field field = WxCpDefaultConfigImpl.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(config, value);
}

/**
* 通过反射获取内部字段值
*/
private Object getField(String fieldName) throws Exception {
Field field = WxCpDefaultConfigImpl.class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(config);
}

/**
* 验证 acquireMsgAuditSdk 在 SDK 有效时能正确返回 SDK 并增加引用计数
*/
@Test
public void testAcquireMsgAuditSdkWhenSdkValid() throws Exception {
long fakeSdk = 12345L;
// 设置一个有效的(未过期的)SDK
setField("msgAuditSdk", fakeSdk);
setField("msgAuditSdkExpiresTime", System.currentTimeMillis() + VALID_EXPIRATION_TIME_OFFSET);
setField("msgAuditSdkRefCount", 0);

long acquired = config.acquireMsgAuditSdk();

Assert.assertEquals(acquired, fakeSdk, "应返回已缓存的有效 SDK");
int refCount = (int) getField("msgAuditSdkRefCount");
Assert.assertEquals(refCount, 1, "引用计数应增加到 1");
}

/**
* 验证 acquireMsgAuditSdk 在 SDK 已过期时返回 0
*/
@Test
public void testAcquireMsgAuditSdkWhenSdkExpired() throws Exception {
long fakeSdk = 12345L;
// 设置已过期的 SDK
setField("msgAuditSdk", fakeSdk);
setField("msgAuditSdkExpiresTime", System.currentTimeMillis() - 1000L);
setField("msgAuditSdkRefCount", 0);

long acquired = config.acquireMsgAuditSdk();

Assert.assertEquals(acquired, 0L, "SDK 已过期,应返回 0");
int refCount = (int) getField("msgAuditSdkRefCount");
Assert.assertEquals(refCount, 0, "引用计数不应改变");
}

/**
* 核心测试:验证当引用计数降为 0 但 SDK 尚未过期时,SDK 不会被销毁
* 这是修复 issue 的关键验证:避免每次 API 调用后频繁销毁和重新初始化 SDK
*/
@Test
public void testReleaseMsgAuditSdkShouldNotDestroyWhenNotExpired() throws Exception {
long fakeSdk = 12345L;
// 设置一个有效的(未过期的)SDK,引用计数为 1
setField("msgAuditSdk", fakeSdk);
setField("msgAuditSdkExpiresTime", System.currentTimeMillis() + VALID_EXPIRATION_TIME_OFFSET);
setField("msgAuditSdkRefCount", 1);

// 释放引用,引用计数应降为 0,但 SDK 尚未过期,不应被销毁
config.releaseMsgAuditSdk(fakeSdk);

long sdkAfterRelease = (long) getField("msgAuditSdk");
int refCountAfterRelease = (int) getField("msgAuditSdkRefCount");

Assert.assertEquals(sdkAfterRelease, fakeSdk, "SDK 尚未过期,引用计数归零后不应被销毁,应继续缓存");
Assert.assertEquals(refCountAfterRelease, 0, "引用计数应为 0");
}

/**
* 验证:SDK 在未过期、引用计数为 0 时,下次调用 acquireMsgAuditSdk 应直接复用,无需重新初始化
* 这是修复后的核心行为:避免频繁初始化
*/
@Test
public void testSdkReuseAfterReleaseWhenNotExpired() throws Exception {
long fakeSdk = 99999L;
// 模拟:SDK 有效,引用计数为 1(正在被使用)
setField("msgAuditSdk", fakeSdk);
setField("msgAuditSdkExpiresTime", System.currentTimeMillis() + VALID_EXPIRATION_TIME_OFFSET);
setField("msgAuditSdkRefCount", 1);

// 模拟方法调用结束,释放引用
config.releaseMsgAuditSdk(fakeSdk);

// 模拟下一次方法调用,应该直接复用缓存的 SDK
long reacquired = config.acquireMsgAuditSdk();

Assert.assertEquals(reacquired, fakeSdk, "SDK 应被复用,而不是返回 0(需要重新初始化)");
int refCount = (int) getField("msgAuditSdkRefCount");
Assert.assertEquals(refCount, 1, "复用后引用计数应为 1");
}

/**
* 验证:多线程场景下,多个并发调用的引用计数正确性
*/
@Test
public void testConcurrentRefCounting() throws Exception {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里方法名/注释强调“多线程/并发”,但实际是单线程顺序调用 acquireMsgAuditSdk()/releaseMsgAuditSdk(),无法覆盖竞态条件下的正确性。可能会让读者误以为已有并发测试覆盖,建议确认是否需要补充真实并发场景或调整描述。

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

long fakeSdk = 77777L;
setField("msgAuditSdk", fakeSdk);
setField("msgAuditSdkExpiresTime", System.currentTimeMillis() + VALID_EXPIRATION_TIME_OFFSET);
setField("msgAuditSdkRefCount", 0);

// 模拟 3 个并发调用同时持有 SDK
long sdk1 = config.acquireMsgAuditSdk();
long sdk2 = config.acquireMsgAuditSdk();
long sdk3 = config.acquireMsgAuditSdk();

Assert.assertEquals(sdk1, fakeSdk);
Assert.assertEquals(sdk2, fakeSdk);
Assert.assertEquals(sdk3, fakeSdk);
Assert.assertEquals((int) getField("msgAuditSdkRefCount"), 3, "应有 3 个引用");

// 逐一释放
config.releaseMsgAuditSdk(fakeSdk);
Assert.assertEquals((int) getField("msgAuditSdkRefCount"), 2, "释放一个后应有 2 个引用");
Assert.assertEquals((long) getField("msgAuditSdk"), fakeSdk, "SDK 仍有引用,不应被销毁");

config.releaseMsgAuditSdk(fakeSdk);
Assert.assertEquals((int) getField("msgAuditSdkRefCount"), 1, "释放两个后应有 1 个引用");

config.releaseMsgAuditSdk(fakeSdk);
Assert.assertEquals((int) getField("msgAuditSdkRefCount"), 0, "全部释放后引用计数应为 0");
// SDK 未过期,不应被销毁
Assert.assertEquals((long) getField("msgAuditSdk"), fakeSdk, "SDK 未过期,全部引用释放后不应被销毁");
}
Comment on lines +128 to +160

/**
* 验证 incrementMsgAuditSdkRefCount 在 SDK 匹配时正确增加引用计数
*/
@Test
public void testIncrementRefCount() throws Exception {
long fakeSdk = 11111L;
setField("msgAuditSdk", fakeSdk);
setField("msgAuditSdkRefCount", 2);

int result = config.incrementMsgAuditSdkRefCount(fakeSdk);

Assert.assertEquals(result, 3, "引用计数应增加到 3");
}

/**
* 验证 incrementMsgAuditSdkRefCount 在 SDK 不匹配时返回 -1
*/
@Test
public void testIncrementRefCountWithWrongSdk() throws Exception {
setField("msgAuditSdk", 11111L);
setField("msgAuditSdkRefCount", 2);

int result = config.incrementMsgAuditSdkRefCount(99999L);

Assert.assertEquals(result, -1, "SDK 不匹配时应返回 -1");
}

/**
* 验证 getMsgAuditSdkRefCount 的正确性
*/
@Test
public void testGetMsgAuditSdkRefCount() throws Exception {
long fakeSdk = 55555L;
setField("msgAuditSdk", fakeSdk);
setField("msgAuditSdkRefCount", 5);

int count = config.getMsgAuditSdkRefCount(fakeSdk);
Assert.assertEquals(count, 5, "应返回正确的引用计数");

int wrongCount = config.getMsgAuditSdkRefCount(99L);
Assert.assertEquals(wrongCount, -1, "SDK 不匹配时应返回 -1");
}
}
1 change: 1 addition & 0 deletions weixin-java-cp/src/test/resources/testng.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<class name="me.chanjar.weixin.cp.api.WxCpMessageRouterTest"/>
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceApacheHttpClientImplTest"/>
<class name="me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImplTest"/>
<class name="me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImplMsgAuditSdkTest"/>
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpTagServiceImplTest"/>
</classes>
</test>
Expand Down
Loading