news 2026/4/16 7:41:34

TokenRetryHelper 详解与 Spring Boot 迁移方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TokenRetryHelper 详解与 Spring Boot 迁移方案

一、TokenRetryHelper 设计解析

1. 核心设计目标

TokenRetryHelper 是一个处理 Token 过期场景的工具类,主要解决以下问题:

  • 当 API 调用返回 Token 过期错误时,自动触发重新登录
  • 重登成功后,自动重试原始请求
  • 多请求并发时遇到 Token 过期,避免重复重登操作
  • 保证整个重登+重试流程的线程安全

2. 关键设计点详解

a) 线程安全机制
privatestaticfinalObjectRELOGIN_LOCK=newObject();privatestaticvolatilebooleanisRelogging=false;privatestaticvolatilelonglastReloginSuccessTime=0;privatestaticfinallongRELOGIN_COOLDOWN_MS=3000;
  • 为什么这样设计?
    • 多个 API 请求可能同时检测到 Token 过期,需要防止并发重登
    • 使用volatile保证变量在多线程间的可见性
    • RELOGIN_LOCK对象锁确保临界区操作的原子性
    • 冷却期机制 (3秒) 防止短时间内重复重登,减轻服务器压力
b) 等待队列设计
privatestaticfinaljava.util.List<ReloginCallback>pendingCallbacks=java.util.Collections.synchronizedList(newjava.util.ArrayList<>());
  • 为什么这样设计?
    • 当一个线程正在重登时,其他检测到 Token 过期的请求不重复触发重登
    • 将这些请求的回调加入等待队列,待重登完成后批量通知
    • 使用synchronizedList保证队列操作的线程安全
    • 解决高并发场景下的资源竞争问题
c) 双重 Token 过期检测
privatestaticbooleanisTokenExpiredCode(intcode){returncode==CODE_TOKEN_EXPIRED||code==CODE_TOKEN_INVALID||code==CODE_TOKEN_ERROR;}privatestaticbooleanisTokenExpiredHttpCode(inthttpCode){returnhttpCode==401||httpCode==403;}
  • 为什么这样设计?
    • 业务层错误码 (1005/4001/3002) 和 HTTP 状态码 (401/403) 都可能表示 Token 问题
    • 双重检测机制确保各种 Token 过期场景都能被正确捕获
    • 适应后端 API 设计的多样性
d) 同步/异步双模式
publicstaticvoidexecuteWithTokenRetry(...){/* 异步模式 */}publicstaticResponse<JsonObject>executeWithTokenRetrySync(...)throwsException{/* 同步模式 */}
  • 为什么这样设计?
    • Android 应用中既有 UI 线程发起的异步请求,也有后台任务需要的同步请求
    • 两种模式共享核心重登逻辑,但处理方式不同
    • 异步模式使用回调链,同步模式使用阻塞等待
e) 令牌获取逻辑的健壮性
StringnewToken=null;if(data!=null&&data.has("token")){newToken=data.get("token").getAsString();}elseif(root.has("token")){newToken=root.get("token").getAsString();}else{// 记录错误日志}
  • 为什么这样设计?
    • 后端 API 可能将 token 放在不同的位置 (data 对象内或根级别)
    • 多层判断保证在 API 结构变化时仍能获取 token
    • 详细的日志记录便于排查 token 获取失败的问题
f) 同步重登的等待机制
while(isRelogging){try{RELOGIN_LOCK.wait(5000);// 最多等待5秒}catch(InterruptedExceptione){Thread.currentThread().interrupt();returnfalse;}// 检查是否在冷却期}
  • 为什么这样设计?
    • 同步方法中,当其他线程正在重登时,当前线程需要等待
    • 使用 wait/notify 机制实现线程间协作
    • 5秒超时防止永久阻塞
    • 中断处理保证线程安全退出
g) 详细的日志与监控
try{ActivationLogger.getInstance(context).logCustomEvent("【TokenRetry】检测到HTTP "+response.code()+",准备重登");}catch(Exceptionignored){}
  • 为什么这样设计?
    • 每个关键步骤都有日志记录,便于问题追踪
    • 使用 try-catch 防止日志记录异常影响主流程
    • 自定义事件日志帮助分析 Token 过期频率和重登成功率

二、Spring Boot 迁移方案

1. 架构设计原则

  • 保持核心重登+重试逻辑不变
  • 适应 Spring 生态的组件和设计模式
  • 利用 Spring 的 AOP 和拦截器机制
  • 使用响应式编程模型替代回调模式

2. 组件设计

a) Token 管理组件
@ComponentpublicclassTokenManager{privatestaticfinallongRELOGIN_COOLDOWN_MS=3000;privatefinalAtomicBooleanisRelogging=newAtomicBoolean(false);privatefinalAtomicLonglastReloginSuccessTime=newAtomicLong(0);privatefinalMap<String,TokenInfo>tokenCache=newConcurrentHashMap<>();privatefinalReentrantLockreloginLock=newReentrantLock();@AutowiredprivateRestTemplaterestTemplate;@AutowiredprivateDeviceServicedeviceService;publicsynchronizedbooleanshouldRelogin(){if(System.currentTimeMillis()-lastReloginSuccessTime.get()<RELOGIN_COOLDOWN_MS){returnfalse;}returnisRelogging.compareAndSet(false,true);}publicvoidcompleteRelogin(booleansuccess,StringnewToken){try{if(success&&newToken!=null){// 保存新tokenlastReloginSuccessTime.set(System.currentTimeMillis());// 更新全局token配置SecurityContextHolder.getContext().setAuthentication(newUsernamePasswordAuthenticationToken(newToken,null));}}finally{isRelogging.set(false);}}// 其他token管理方法...}
b) 拦截器设计 (处理 Token 过期)
@ComponentpublicclassTokenExpirationInterceptorimplementsClientHttpRequestInterceptor{@AutowiredprivateTokenManagertokenManager;@AutowiredprivateReloginServicereloginService;@OverridepublicClientHttpResponseintercept(HttpRequestrequest,byte[]body,ClientHttpRequestExecutionexecution)throwsIOException{// 1. 添加当前token到请求头StringcurrentToken=getCurrentToken();request.getHeaders().setBearerAuth(currentToken);try{ClientHttpResponseresponse=execution.execute(request,body);// 2. 检查是否token过期if(isTokenExpired(response)){// 3. 触发重登if(reloginService.relogin()){// 4. 重登成功,使用新token重试请求StringnewToken=getCurrentToken();request.getHeaders().setBearerAuth(newToken);returnexecution.execute(request,body);}}returnresponse;}catch(Exceptione){thrownewIOException("Request execution failed",e);}}privatebooleanisTokenExpired(ClientHttpResponseresponse)throwsIOException{intstatusCode=response.getStatusCode().value();if(statusCode==401||statusCode==403){returntrue;}// 检查业务错误码ObjectMappermapper=newObjectMapper();JsonNodebody=mapper.readTree(response.getBody());intcode=body.has("code")?body.get("code").asInt():-1;returncode==1005||code==4001||code==3002;}privateStringgetCurrentToken(){Authenticationauth=SecurityContextHolder.getContext().getAuthentication();returnauth!=null?auth.getCredentials().toString():"";}}
c) 重登服务
@ServicepublicclassReloginService{@AutowiredprivateTokenManagertokenManager;@AutowiredprivateRestTemplaterestTemplate;@Value("${api.base-url}")privateStringbaseUrl;@Value("${api.login-endpoint}")privateStringloginEndpoint;publicbooleanrelogin(){reloginLock.lock();try{// 检查冷却期if(!tokenManager.shouldRelogin()){returntrue;// 在冷却期内,视为已登录}try{// 获取设备信息Devicedevice=deviceService.getLatestDevice();if(device==null||device.getDeviceOriginalId()==0){log.error("Relogin failed: invalid device information");returnfalse;}// 构建登录请求Map<String,Object>payload=newHashMap<>();payload.put("deviceOriginalId",device.getDeviceOriginalId());// 执行登录ResponseEntity<JsonNode>response=restTemplate.postForEntity(baseUrl+loginEndpoint,payload,JsonNode.class);if(response.getStatusCode().is2xxSuccessful()&&response.getBody()!=null){JsonNoderesponseBody=response.getBody();intapiCode=responseBody.has("code")?getSafeIntValue(responseBody,"code"):-1;if(apiCode==0||apiCode==200){// 提取token(支持多种结构)StringnewToken=extractToken(responseBody);if(newToken!=null&&!newToken.trim().isEmpty()){tokenManager.completeRelogin(true,newToken);log.info("Relogin successful, token updated");returntrue;}}}log.error("Relogin failed: invalid response from server");returnfalse;}catch(Exceptione){log.error("Relogin failed: exception occurred",e);returnfalse;}finally{if(!tokenManager.isRelogging().get()){tokenManager.completeRelogin(false,null);}}}finally{reloginLock.unlock();}}privateStringextractToken(JsonNoderoot){if(root.has("data")&&root.get("data").isObject()&&root.get("data").has("token")){returnroot.get("data").get("token").asText();}elseif(root.has("token")){returnroot.get("token").asText();}returnnull;}privateintgetSafeIntValue(JsonNodenode,Stringfield){try{returnnode.get(field).asInt();}catch(Exceptione){try{returnInteger.parseInt(node.get(field).asText());}catch(Exceptionignore){return-1;}}}}
d) 配置 RestTemplate 使用拦截器
@ConfigurationpublicclassApiConfig{@BeanpublicRestTemplaterestTemplate(TokenExpirationInterceptorinterceptor){RestTemplaterestTemplate=newRestTemplate();List<ClientHttpRequestInterceptor>interceptors=newArrayList<>();interceptors.add(interceptor);restTemplate.setInterceptors(interceptors);returnrestTemplate;}}
e) 响应式版本 (WebFlux)
@ComponentpublicclassReactiveTokenRetryFilterimplementsWebFilter{@AutowiredprivateReactiveTokenManagertokenManager;@AutowiredprivateReactiveReloginServicereloginService;@OverridepublicMono<Void>filter(ServerWebExchangeexchange,WebFilterChainchain){// 1. 获取当前tokenStringcurrentToken=tokenManager.getCurrentToken();// 2. 添加到请求头exchange.getRequest().mutate().header(HttpHeaders.AUTHORIZATION,"Bearer "+currentToken).build();// 3. 执行请求并处理token过期returnchain.filter(exchange).onErrorResume(WebClientResponseException.Unauthorized.class,e->handleTokenExpiration(exchange,chain)).onErrorResume(WebClientResponseException.Forbidden.class,e->handleTokenExpiration(exchange,chain));}privateMono<Void>handleTokenExpiration(ServerWebExchangeexchange,WebFilterChainchain){returnreloginService.relogin().flatMap(success->{if(success){// 重登成功,使用新token重试StringnewToken=tokenManager.getCurrentToken();exchange.getRequest().mutate().header(HttpHeaders.AUTHORIZATION,"Bearer "+newToken).build();returnchain.filter(exchange);}else{// 重登失败,返回401exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);returnexchange.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

    @Retryable(value={TokenExpiredException.class},maxAttempts=2)publicResponseEntity<JsonNode>makeApiCall(){// API调用逻辑}@RecoverpublicResponseEntity<JsonNode>recover(TokenExpiredExceptione){// 重登逻辑}
  2. 令牌刷新优化

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

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

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

三、总结

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 7:41:29

网络安全扫描利器:gau工具快速上手指南

网络安全扫描利器&#xff1a;gau工具快速上手指南 【免费下载链接】gau 项目地址: https://gitcode.com/gh_mirrors/ga/gau 在网络安全评估中&#xff0c;你是否曾因无法发现隐藏的安全漏洞而苦恼&#xff1f;一家知名电商网站在安全审计中&#xff0c;通过gau工具发现…

作者头像 李华
网站建设 2026/4/11 12:28:11

ms-swift支持MoE架构模型训练,结合ETP/VPP并行策略加速达10倍

ms-swift 支持 MoE 架构模型训练&#xff0c;结合 ETP/VPP 并行策略实现 10 倍加速 在当前大模型技术飞速演进的背景下&#xff0c;参数规模已从数十亿迈向万亿级别。随着 Qwen-MoE、DeepSeek-MoE 等稀疏架构的兴起&#xff0c;如何高效训练这些“巨无霸”模型&#xff0c;成为…

作者头像 李华
网站建设 2026/4/14 1:10:20

深度学习知识追踪实战指南:5步构建智能教育分析系统

深度学习知识追踪实战指南&#xff1a;5步构建智能教育分析系统 【免费下载链接】pykt-toolkit 项目地址: https://gitcode.com/gh_mirrors/py/pykt-toolkit 在当今数字化教育时代&#xff0c;准确评估学生的学习状态和知识掌握程度变得至关重要。pykt-toolkit作为基于…

作者头像 李华
网站建设 2026/4/1 16:11:56

Ghost Downloader 3:重新定义智能跨平台下载体验的技术实践

Ghost Downloader 3&#xff1a;重新定义智能跨平台下载体验的技术实践 【免费下载链接】Ghost-Downloader-3 A multi-threading async downloader with QThread based on PyQt/PySide. 跨平台 多线程下载器 协程下载器 项目地址: https://gitcode.com/GitHub_Trending/gh/Gh…

作者头像 李华
网站建设 2026/4/13 12:48:30

域控宕机!如何强制夺取五大角色恢复业务?

背景&#xff1a; 近年来&#xff0c;针对企业的勒索病毒攻击愈发猖獗。试想一下&#xff0c;如果核心的Active Directory&#xff08;AD&#xff09;域控制器被勒索加密或硬件损坏无法启动&#xff0c;导致全公司认证瘫痪&#xff0c;作为安全/运维人员&#xff0c;该如何快速…

作者头像 李华
网站建设 2026/4/9 4:52:09

FactoryBluePrints:戴森球计划终极蓝图库完整使用指南

FactoryBluePrints&#xff1a;戴森球计划终极蓝图库完整使用指南 【免费下载链接】FactoryBluePrints 游戏戴森球计划的**工厂**蓝图仓库 项目地址: https://gitcode.com/GitHub_Trending/fa/FactoryBluePrints 你是否曾经在戴森球计划中遭遇这样的困境&#xff1f;精心…

作者头像 李华