TokenRetryHelper 详解与 Spring Boot 迁移方案

一、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 实现的核心区别

  1. 线程模型
    • Android:使用锁+等待队列处理多线程
    • Spring Boot:使用 Spring 的线程池和 @Async 注解,或响应式流
  2. 存储机制
    • Android:SharedPreferences 存储 token
    • Spring Boot:SecurityContext 或内存缓存,可能结合数据库
  3. 网络请求
    • Android:Retrofit + OkHttp
    • Spring Boot:RestTemplate/WebClient + 拦截器
  4. 重试机制
    • Android:回调链模式
    • Spring Boot:AOP 或响应式操作符 (retryWhen)
  5. 上下文管理
    • Android:显式传递 Context
    • Spring Boot:依赖注入和上下文管理

4. 优化建议

  1. 使用 Spring Retry

    java 复制代码
    @Retryable(value = {TokenExpiredException.class}, maxAttempts = 2)
    public ResponseEntity<JsonNode> makeApiCall() {
        // API调用逻辑
    }
    
    @Recover
    public ResponseEntity<JsonNode> recover(TokenExpiredException e) {
        // 重登逻辑
    }
  2. 令牌刷新优化

    • 实现在令牌真正过期前主动刷新,避免请求失败
    • 使用定时任务提前刷新即将过期的令牌
  3. 熔断机制

    • 集成 Resilience4j,当重登连续失败时触发熔断
    • 防止服务器压力过大导致雪崩
  4. 监控指标

    • 使用 Micrometer 记录重登频率、成功率
    • 集成 Prometheus + Grafana 监控面板

三、总结

TokenRetryHelper 的设计充分考虑了移动端环境的特点:网络不稳定、资源受限、多线程环境复杂。在迁移到 Spring Boot 时,我们需要保留其核心思想------自动处理 Token 过期、避免重复重登、线程安全,同时充分利用 Spring 生态的特性,如 AOP、依赖注入、响应式编程等,构建更简洁、可维护的解决方案。

通过拦截器+Token管理器的组合模式,我们可以在保持业务代码干净的同时,透明地处理 Token 过期问题,这比 Android 实现更加优雅,也更符合后端服务的设计理念。

相关推荐
無限進步D3 小时前
Java 运行原理
java·开发语言·入门
難釋懷3 小时前
安装Canal
java
是苏浙3 小时前
JDK17新增特性
java·开发语言
不光头强3 小时前
spring cloud知识总结
后端·spring·spring cloud
GetcharZp6 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端
阿里加多7 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
likerhood7 小时前
java中`==`和`.equals()`区别
java·开发语言·python
小小李程序员7 小时前
Langchain4j工具调用获取不到ThreadLocal
java·后端·ai