Skip to content

Commit e50d1c2

Browse files
Copilotbinarywang
andcommitted
修复 Redis 命令被中断时的 accessToken 异常问题
Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
1 parent 0e9df9c commit e50d1c2

File tree

4 files changed

+258
-7
lines changed

4 files changed

+258
-7
lines changed

weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void lock() {
4646
try {
4747
Thread.sleep(1000);
4848
} catch (InterruptedException e) {
49-
// Ignore
49+
Thread.currentThread().interrupt();
5050
}
5151
}
5252
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package me.chanjar.weixin.common.util.locks;
2+
3+
import org.mockito.Mockito;
4+
import org.springframework.data.redis.core.StringRedisTemplate;
5+
import org.springframework.data.redis.core.ValueOperations;
6+
import org.testng.Assert;
7+
import org.testng.annotations.BeforeMethod;
8+
import org.testng.annotations.Test;
9+
10+
import java.util.concurrent.TimeUnit;
11+
import java.util.concurrent.atomic.AtomicBoolean;
12+
13+
/**
14+
* 测试 RedisTemplateSimpleDistributedLock 的线程中断处理行为
15+
*
16+
* @author GitHub Copilot
17+
*/
18+
public class RedisTemplateSimpleDistributedLockInterruptTest {
19+
20+
private StringRedisTemplate mockRedisTemplate;
21+
private ValueOperations<String, String> mockValueOps;
22+
private RedisTemplateSimpleDistributedLock lock;
23+
24+
@BeforeMethod
25+
@SuppressWarnings("unchecked")
26+
public void setUp() {
27+
mockRedisTemplate = Mockito.mock(StringRedisTemplate.class);
28+
mockValueOps = Mockito.mock(ValueOperations.class);
29+
Mockito.when(mockRedisTemplate.opsForValue()).thenReturn(mockValueOps);
30+
lock = new RedisTemplateSimpleDistributedLock(mockRedisTemplate, "test_interrupt_lock", 60000);
31+
}
32+
33+
/**
34+
* 测试 lock() 在 Thread.sleep 被中断时应恢复线程中断标志
35+
* <p>
36+
* 修复前:InterruptedException 被忽略(// Ignore),线程中断标志丢失
37+
* 修复后:调用 Thread.currentThread().interrupt() 恢复中断标志
38+
* </p>
39+
*/
40+
@Test(description = "lock() 方法在中断时应恢复线程中断标志")
41+
public void testLockRestoresInterruptedFlagAfterSleepInterruption() throws InterruptedException {
42+
AtomicBoolean interruptedFlagAfterLock = new AtomicBoolean(false);
43+
44+
// 第一次 setIfAbsent 返回 false(模拟锁被占用),第二次返回 true(模拟锁释放)
45+
Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(),
46+
Mockito.anyLong(), Mockito.any(TimeUnit.class)))
47+
.thenReturn(false)
48+
.thenReturn(true);
49+
// get() 返回不同的值,确保不走可重入路径
50+
Mockito.when(mockValueOps.get(Mockito.anyString())).thenReturn("other-value");
51+
52+
Thread testThread = new Thread(() -> {
53+
// 设置中断标志
54+
Thread.currentThread().interrupt();
55+
// 调用 lock(),第一次 tryLock 失败,sleep 会因中断标志立即抛出 InterruptedException
56+
lock.lock();
57+
interruptedFlagAfterLock.set(Thread.currentThread().isInterrupted());
58+
});
59+
60+
testThread.start();
61+
testThread.join(5000);
62+
63+
// 线程应该已经完成(不会永远阻塞)
64+
Assert.assertFalse(testThread.isAlive(), "线程应该已完成");
65+
// 关键验证:中断标志应被恢复(而非被忽略丢失)
66+
Assert.assertTrue(interruptedFlagAfterLock.get(), "lock()执行后线程中断标志应被恢复");
67+
}
68+
69+
/**
70+
* 测试 tryLock() 在 Redis 正常响应时的基本行为
71+
*/
72+
@Test(description = "tryLock() 成功获取锁时应返回 true")
73+
public void testTryLockSuccessfully() {
74+
Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(),
75+
Mockito.anyLong(), Mockito.any(TimeUnit.class)))
76+
.thenReturn(true);
77+
78+
boolean result = lock.tryLock();
79+
80+
Assert.assertTrue(result, "应成功获取锁");
81+
Assert.assertNotNull(lock.getLockSecretValue(), "锁值不应为null");
82+
}
83+
84+
/**
85+
* 测试 tryLock() 在锁已被其他线程持有时应返回 false
86+
*/
87+
@Test(description = "锁被占用时 tryLock() 应返回 false")
88+
public void testTryLockWhenLockHeld() {
89+
Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(),
90+
Mockito.anyLong(), Mockito.any(TimeUnit.class)))
91+
.thenReturn(false);
92+
Mockito.when(mockValueOps.get(Mockito.anyString())).thenReturn("other-lock-value");
93+
94+
boolean result = lock.tryLock();
95+
96+
Assert.assertFalse(result, "锁被占用时应返回false");
97+
}
98+
}

weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package me.chanjar.weixin.cp.config.impl;
22

33
import lombok.NonNull;
4+
import lombok.extern.slf4j.Slf4j;
45
import me.chanjar.weixin.common.bean.WxAccessToken;
56
import me.chanjar.weixin.common.redis.WxRedisOps;
67
import org.apache.commons.lang3.StringUtils;
@@ -12,6 +13,7 @@
1213
* @author yl
1314
* created on 2023/04/23
1415
*/
16+
@Slf4j
1517
public abstract class AbstractWxCpInRedisConfigImpl extends WxCpDefaultConfigImpl {
1618
private static final long serialVersionUID = 7157341535439380615L;
1719
/**
@@ -120,8 +122,15 @@ public String getAccessToken() {
120122

121123
@Override
122124
public boolean isAccessTokenExpired() {
123-
Long expire = redisOps.getExpire(this.accessTokenKey);
124-
return expire == null || expire < 2;
125+
try {
126+
Long expire = redisOps.getExpire(this.accessTokenKey);
127+
return expire == null || expire < 2;
128+
} catch (Exception e) {
129+
log.warn("获取access_token过期时间时发生异常,将视为已过期以触发刷新,异常信息: {}", e.getMessage());
130+
// 清除中断标志,确保后续的锁获取和token刷新操作能够正常执行
131+
Thread.interrupted();
132+
return true;
133+
}
125134
}
126135

127136
@Override
@@ -146,8 +155,14 @@ public String getJsapiTicket() {
146155

147156
@Override
148157
public boolean isJsapiTicketExpired() {
149-
Long expire = redisOps.getExpire(this.jsapiTicketKey);
150-
return expire == null || expire < 2;
158+
try {
159+
Long expire = redisOps.getExpire(this.jsapiTicketKey);
160+
return expire == null || expire < 2;
161+
} catch (Exception e) {
162+
log.warn("获取jsapi_ticket过期时间时发生异常,将视为已过期,异常信息: {}", e.getMessage());
163+
Thread.interrupted();
164+
return true;
165+
}
151166
}
152167

153168
@Override
@@ -177,8 +192,14 @@ public String getAgentJsapiTicket() {
177192

178193
@Override
179194
public boolean isAgentJsapiTicketExpired() {
180-
Long expire = redisOps.getExpire(this.agentJsapiTicketKey);
181-
return expire == null || expire < 2;
195+
try {
196+
Long expire = redisOps.getExpire(this.agentJsapiTicketKey);
197+
return expire == null || expire < 2;
198+
} catch (Exception e) {
199+
log.warn("获取agent_jsapi_ticket过期时间时发生异常,将视为已过期,异常信息: {}", e.getMessage());
200+
Thread.interrupted();
201+
return true;
202+
}
182203
}
183204

184205
@Override
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package me.chanjar.weixin.cp.config.impl;
2+
3+
import me.chanjar.weixin.common.redis.WxRedisOps;
4+
import org.mockito.Mockito;
5+
import org.testng.Assert;
6+
import org.testng.annotations.BeforeMethod;
7+
import org.testng.annotations.Test;
8+
9+
import java.util.concurrent.locks.Lock;
10+
import java.util.concurrent.locks.ReentrantLock;
11+
12+
/**
13+
* 测试 AbstractWxCpInRedisConfigImpl 对 Redis 异常的容错处理
14+
*
15+
* @author GitHub Copilot
16+
*/
17+
public class AbstractWxCpInRedisConfigImplTest {
18+
19+
private WxRedisOps mockRedisOps;
20+
private AbstractWxCpInRedisConfigImpl config;
21+
22+
@BeforeMethod
23+
public void setUp() {
24+
mockRedisOps = Mockito.mock(WxRedisOps.class);
25+
Mockito.when(mockRedisOps.getLock(Mockito.anyString()))
26+
.thenReturn(new ReentrantLock());
27+
28+
config = new AbstractWxCpInRedisConfigImpl(mockRedisOps, "test") {
29+
// 使用匿名类提供具体实现用于测试
30+
};
31+
config.setCorpId("testCorpId");
32+
config.setAgentId(1);
33+
}
34+
35+
/**
36+
* 测试当 Redis getExpire 抛出异常时,isAccessTokenExpired() 应返回 true(视为已过期)
37+
*/
38+
@Test
39+
public void testIsAccessTokenExpiredWhenRedisThrowsException() {
40+
Mockito.when(mockRedisOps.getExpire(Mockito.anyString()))
41+
.thenThrow(new RuntimeException("Redis command interrupted"));
42+
43+
boolean expired = config.isAccessTokenExpired();
44+
45+
Assert.assertTrue(expired, "Redis异常时应将token视为已过期");
46+
Assert.assertFalse(Thread.currentThread().isInterrupted(), "处理异常后线程中断标志应被清除");
47+
}
48+
49+
/**
50+
* 测试当线程中断状态已设置时,Redis 调用抛出异常,isAccessTokenExpired() 应处理并清除中断标志
51+
*/
52+
@Test
53+
public void testIsAccessTokenExpiredClearsInterruptedFlag() {
54+
Mockito.when(mockRedisOps.getExpire(Mockito.anyString()))
55+
.thenThrow(new RuntimeException("Redis command interrupted"));
56+
57+
// 设置线程中断标志
58+
Thread.currentThread().interrupt();
59+
60+
boolean expired = config.isAccessTokenExpired();
61+
62+
Assert.assertTrue(expired, "Redis异常时应将token视为已过期");
63+
// 中断标志应该被清除,允许后续操作正常进行
64+
Assert.assertFalse(Thread.currentThread().isInterrupted(), "处理异常后线程中断标志应被清除");
65+
}
66+
67+
/**
68+
* 测试正常情况下 isAccessTokenExpired() 的行为
69+
*/
70+
@Test
71+
public void testIsAccessTokenExpiredWhenTokenValid() {
72+
// 返回60秒后过期(未过期)
73+
Mockito.when(mockRedisOps.getExpire(Mockito.anyString())).thenReturn(60L);
74+
75+
boolean expired = config.isAccessTokenExpired();
76+
77+
Assert.assertFalse(expired, "token未过期时应返回false");
78+
}
79+
80+
/**
81+
* 测试 isAccessTokenExpired() 当 expire 为 null 时视为已过期
82+
*/
83+
@Test
84+
public void testIsAccessTokenExpiredWhenExpireIsNull() {
85+
Mockito.when(mockRedisOps.getExpire(Mockito.anyString())).thenReturn(null);
86+
87+
boolean expired = config.isAccessTokenExpired();
88+
89+
Assert.assertTrue(expired, "expire为null时应视为已过期");
90+
}
91+
92+
/**
93+
* 测试当 Redis getExpire 抛出异常时,isJsapiTicketExpired() 应返回 true(视为已过期)
94+
*/
95+
@Test
96+
public void testIsJsapiTicketExpiredWhenRedisThrowsException() {
97+
Mockito.when(mockRedisOps.getExpire(Mockito.anyString()))
98+
.thenThrow(new RuntimeException("Redis command interrupted"));
99+
100+
boolean expired = config.isJsapiTicketExpired();
101+
102+
Assert.assertTrue(expired, "Redis异常时应将jsapi_ticket视为已过期");
103+
Assert.assertFalse(Thread.currentThread().isInterrupted(), "处理异常后线程中断标志应被清除");
104+
}
105+
106+
/**
107+
* 测试当 Redis getExpire 抛出异常时,isAgentJsapiTicketExpired() 应返回 true(视为已过期)
108+
*/
109+
@Test
110+
public void testIsAgentJsapiTicketExpiredWhenRedisThrowsException() {
111+
Mockito.when(mockRedisOps.getExpire(Mockito.anyString()))
112+
.thenThrow(new RuntimeException("Redis command interrupted"));
113+
114+
boolean expired = config.isAgentJsapiTicketExpired();
115+
116+
Assert.assertTrue(expired, "Redis异常时应将agent_jsapi_ticket视为已过期");
117+
Assert.assertFalse(Thread.currentThread().isInterrupted(), "处理异常后线程中断标志应被清除");
118+
}
119+
120+
/**
121+
* 测试提供自定义 Lock 实现时 getAccessTokenLock() 返回正确的锁
122+
*/
123+
@Test
124+
public void testGetAccessTokenLockReturnsMockedLock() {
125+
Lock mockLock = Mockito.mock(Lock.class);
126+
Mockito.when(mockRedisOps.getLock(Mockito.anyString())).thenReturn(mockLock);
127+
128+
Lock lock = config.getAccessTokenLock();
129+
130+
Assert.assertNotNull(lock, "获取到的锁不应为null");
131+
}
132+
}

0 commit comments

Comments
 (0)