一、TokenRetryHelper 设计解析
1. 核心设计目标
TokenRetryHelper 是一个处理 Token 过期场景的工具类,主要解决以下问题:
- 当 API 调用返回 Token 过期错误时,自动触发重新登录
- 重登成功后,自动重试原始请求
- 多请求并发时遇到 Token 过期,避免重复重登操作
- 保证整个重登+重试流程的线程安全
2. 关键设计点详解
a) 线程安全机制
java
private static final Object RELOGIN_LOCK = new Object();
private static volatile boolean isRelogging = false;
private static volatile long lastReloginSuccessTime = 0;
private static final long RELOGIN_COOLDOWN_MS = 3000;
- 为什么这样设计?
- 多个 API 请求可能同时检测到 Token 过期,需要防止并发重登
- 使用
volatile保证变量在多线程间的可见性 RELOGIN_LOCK对象锁确保临界区操作的原子性- 冷却期机制 (3秒) 防止短时间内重复重登,减轻服务器压力
b) 等待队列设计
java
private static final java.util.List<ReloginCallback> pendingCallbacks =
java.util.Collections.synchronizedList(new java.util.ArrayList<>());
- 为什么这样设计?
- 当一个线程正在重登时,其他检测到 Token 过期的请求不重复触发重登
- 将这些请求的回调加入等待队列,待重登完成后批量通知
- 使用
synchronizedList保证队列操作的线程安全 - 解决高并发场景下的资源竞争问题
c) 双重 Token 过期检测
java
private static boolean isTokenExpiredCode(int code) {
return code == CODE_TOKEN_EXPIRED || code == CODE_TOKEN_INVALID || code == CODE_TOKEN_ERROR;
}
private static boolean isTokenExpiredHttpCode(int httpCode) {
return httpCode == 401 || httpCode == 403;
}
- 为什么这样设计?
- 业务层错误码 (1005/4001/3002) 和 HTTP 状态码 (401/403) 都可能表示 Token 问题
- 双重检测机制确保各种 Token 过期场景都能被正确捕获
- 适应后端 API 设计的多样性
d) 同步/异步双模式
java
public static void executeWithTokenRetry(...) { /* 异步模式 */ }
public static Response<JsonObject> executeWithTokenRetrySync(...) throws Exception { /* 同步模式 */ }
- 为什么这样设计?
- Android 应用中既有 UI 线程发起的异步请求,也有后台任务需要的同步请求
- 两种模式共享核心重登逻辑,但处理方式不同
- 异步模式使用回调链,同步模式使用阻塞等待
e) 令牌获取逻辑的健壮性
java
String newToken = null;
if (data != null && data.has("token")) {
newToken = data.get("token").getAsString();
} else if (root.has("token")) {
newToken = root.get("token").getAsString();
} else {
// 记录错误日志
}
- 为什么这样设计?
- 后端 API 可能将 token 放在不同的位置 (data 对象内或根级别)
- 多层判断保证在 API 结构变化时仍能获取 token
- 详细的日志记录便于排查 token 获取失败的问题
f) 同步重登的等待机制
java
while (isRelogging) {
try {
RELOGIN_LOCK.wait(5000); // 最多等待5秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
// 检查是否在冷却期
}
- 为什么这样设计?
- 同步方法中,当其他线程正在重登时,当前线程需要等待
- 使用 wait/notify 机制实现线程间协作
- 5秒超时防止永久阻塞
- 中断处理保证线程安全退出
g) 详细的日志与监控
java
try { ActivationLogger.getInstance(context).logCustomEvent("【TokenRetry】检测到HTTP " + response.code() + ",准备重登"); } catch (Exception ignored) {}
- 为什么这样设计?
- 每个关键步骤都有日志记录,便于问题追踪
- 使用 try-catch 防止日志记录异常影响主流程
- 自定义事件日志帮助分析 Token 过期频率和重登成功率
二、Spring Boot 迁移方案
1. 架构设计原则
- 保持核心重登+重试逻辑不变
- 适应 Spring 生态的组件和设计模式
- 利用 Spring 的 AOP 和拦截器机制
- 使用响应式编程模型替代回调模式
2. 组件设计
a) Token 管理组件
java
@Component
public class TokenManager {
private static final long RELOGIN_COOLDOWN_MS = 3000;
private final AtomicBoolean isRelogging = new AtomicBoolean(false);
private final AtomicLong lastReloginSuccessTime = new AtomicLong(0);
private final Map<String, TokenInfo> tokenCache = new ConcurrentHashMap<>();
private final ReentrantLock reloginLock = new ReentrantLock();
@Autowired
private RestTemplate restTemplate;
@Autowired
private DeviceService deviceService;
public synchronized boolean shouldRelogin() {
if (System.currentTimeMillis() - lastReloginSuccessTime.get() < RELOGIN_COOLDOWN_MS) {
return false;
}
return isRelogging.compareAndSet(false, true);
}
public void completeRelogin(boolean success, String newToken) {
try {
if (success && newToken != null) {
// 保存新token
lastReloginSuccessTime.set(System.currentTimeMillis());
// 更新全局token配置
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(newToken, null)
);
}
} finally {
isRelogging.set(false);
}
}
// 其他token管理方法...
}
b) 拦截器设计 (处理 Token 过期)
java
@Component
public class TokenExpirationInterceptor implements ClientHttpRequestInterceptor {
@Autowired
private TokenManager tokenManager;
@Autowired
private ReloginService reloginService;
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// 1. 添加当前token到请求头
String currentToken = getCurrentToken();
request.getHeaders().setBearerAuth(currentToken);
try {
ClientHttpResponse response = execution.execute(request, body);
// 2. 检查是否token过期
if (isTokenExpired(response)) {
// 3. 触发重登
if (reloginService.relogin()) {
// 4. 重登成功,使用新token重试请求
String newToken = getCurrentToken();
request.getHeaders().setBearerAuth(newToken);
return execution.execute(request, body);
}
}
return response;
} catch (Exception e) {
throw new IOException("Request execution failed", e);
}
}
private boolean isTokenExpired(ClientHttpResponse response) throws IOException {
int statusCode = response.getStatusCode().value();
if (statusCode == 401 || statusCode == 403) {
return true;
}
// 检查业务错误码
ObjectMapper mapper = new ObjectMapper();
JsonNode body = mapper.readTree(response.getBody());
int code = body.has("code") ? body.get("code").asInt() : -1;
return code == 1005 || code == 4001 || code == 3002;
}
private String getCurrentToken() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getCredentials().toString() : "";
}
}
c) 重登服务
java
@Service
public class ReloginService {
@Autowired
private TokenManager tokenManager;
@Autowired
private RestTemplate restTemplate;
@Value("${api.base-url}")
private String baseUrl;
@Value("${api.login-endpoint}")
private String loginEndpoint;
public boolean relogin() {
reloginLock.lock();
try {
// 检查冷却期
if (!tokenManager.shouldRelogin()) {
return true; // 在冷却期内,视为已登录
}
try {
// 获取设备信息
Device device = deviceService.getLatestDevice();
if (device == null || device.getDeviceOriginalId() == 0) {
log.error("Relogin failed: invalid device information");
return false;
}
// 构建登录请求
Map<String, Object> payload = new HashMap<>();
payload.put("deviceOriginalId", device.getDeviceOriginalId());
// 执行登录
ResponseEntity<JsonNode> response = restTemplate.postForEntity(
baseUrl + loginEndpoint,
payload,
JsonNode.class
);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
JsonNode responseBody = response.getBody();
int apiCode = responseBody.has("code") ?
getSafeIntValue(responseBody, "code") : -1;
if (apiCode == 0 || apiCode == 200) {
// 提取token(支持多种结构)
String newToken = extractToken(responseBody);
if (newToken != null && !newToken.trim().isEmpty()) {
tokenManager.completeRelogin(true, newToken);
log.info("Relogin successful, token updated");
return true;
}
}
}
log.error("Relogin failed: invalid response from server");
return false;
} catch (Exception e) {
log.error("Relogin failed: exception occurred", e);
return false;
} finally {
if (!tokenManager.isRelogging().get()) {
tokenManager.completeRelogin(false, null);
}
}
} finally {
reloginLock.unlock();
}
}
private String extractToken(JsonNode root) {
if (root.has("data") && root.get("data").isObject() &&
root.get("data").has("token")) {
return root.get("data").get("token").asText();
} else if (root.has("token")) {
return root.get("token").asText();
}
return null;
}
private int getSafeIntValue(JsonNode node, String field) {
try {
return node.get(field).asInt();
} catch (Exception e) {
try {
return Integer.parseInt(node.get(field).asText());
} catch (Exception ignore) {
return -1;
}
}
}
}
d) 配置 RestTemplate 使用拦截器
java
@Configuration
public class ApiConfig {
@Bean
public RestTemplate restTemplate(TokenExpirationInterceptor interceptor) {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(interceptor);
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
e) 响应式版本 (WebFlux)
java
@Component
public class ReactiveTokenRetryFilter implements WebFilter {
@Autowired
private ReactiveTokenManager tokenManager;
@Autowired
private ReactiveReloginService reloginService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// 1. 获取当前token
String currentToken = tokenManager.getCurrentToken();
// 2. 添加到请求头
exchange.getRequest().mutate()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + currentToken)
.build();
// 3. 执行请求并处理token过期
return chain.filter(exchange)
.onErrorResume(WebClientResponseException.Unauthorized.class, e ->
handleTokenExpiration(exchange, chain))
.onErrorResume(WebClientResponseException.Forbidden.class, e ->
handleTokenExpiration(exchange, chain));
}
private Mono<Void> handleTokenExpiration(ServerWebExchange exchange, WebFilterChain chain) {
return reloginService.relogin()
.flatMap(success -> {
if (success) {
// 重登成功,使用新token重试
String newToken = tokenManager.getCurrentToken();
exchange.getRequest().mutate()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + newToken)
.build();
return chain.filter(exchange);
} else {
// 重登失败,返回401
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
});
}
}
3. 与原 Android 实现的核心区别
- 线程模型 :
- Android:使用锁+等待队列处理多线程
- Spring Boot:使用 Spring 的线程池和 @Async 注解,或响应式流
- 存储机制 :
- Android:SharedPreferences 存储 token
- Spring Boot:SecurityContext 或内存缓存,可能结合数据库
- 网络请求 :
- Android:Retrofit + OkHttp
- Spring Boot:RestTemplate/WebClient + 拦截器
- 重试机制 :
- Android:回调链模式
- Spring Boot:AOP 或响应式操作符 (retryWhen)
- 上下文管理 :
- Android:显式传递 Context
- Spring Boot:依赖注入和上下文管理
4. 优化建议
-
使用 Spring Retry:
java@Retryable(value = {TokenExpiredException.class}, maxAttempts = 2) public ResponseEntity<JsonNode> makeApiCall() { // API调用逻辑 } @Recover public ResponseEntity<JsonNode> recover(TokenExpiredException e) { // 重登逻辑 } -
令牌刷新优化:
- 实现在令牌真正过期前主动刷新,避免请求失败
- 使用定时任务提前刷新即将过期的令牌
-
熔断机制:
- 集成 Resilience4j,当重登连续失败时触发熔断
- 防止服务器压力过大导致雪崩
-
监控指标:
- 使用 Micrometer 记录重登频率、成功率
- 集成 Prometheus + Grafana 监控面板
三、总结
TokenRetryHelper 的设计充分考虑了移动端环境的特点:网络不稳定、资源受限、多线程环境复杂。在迁移到 Spring Boot 时,我们需要保留其核心思想------自动处理 Token 过期、避免重复重登、线程安全,同时充分利用 Spring 生态的特性,如 AOP、依赖注入、响应式编程等,构建更简洁、可维护的解决方案。
通过拦截器+Token管理器的组合模式,我们可以在保持业务代码干净的同时,透明地处理 Token 过期问题,这比 Android 实现更加优雅,也更符合后端服务的设计理念。