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 实现更加优雅,也更符合后端服务的设计理念。

相关推荐
毕设源码-赖学姐21 小时前
【开题答辩全过程】以 音像租借管理系统为例,包含答辩的问题和答案
java
云上小朱21 小时前
软件部署-在k8s部署Hadoop集群
后端
镜花水月linyi21 小时前
Cookie、Session、JWT 的区别?
后端·面试
小宇的天下21 小时前
Calibre 3Dstack --每日一个命令day7【Centers】(3-7)
java·服务器·数据库
用户03048059126321 小时前
Spring Boot 配置文件加载大揭秘:优先级覆盖与互补合并机制详解
java·后端
青莲84321 小时前
Java内存回收机制(GC)完整详解
java·前端·面试
CRUD酱1 天前
微服务分模块后怎么跨模块访问资源
java·分布式·微服务·中间件·java-ee
Knight_AL1 天前
MinIO 入门实战:Docker 安装 + Spring Boot 文件上传(公有 / 私有)
spring boot·docker·容器
gAlAxy...1 天前
5 种 SpringBoot 项目创建方式
java·spring boot·后端