news 2026/8/3 11:23:03

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/7/28 15:10:45

戴森球计划工厂建设终极指南:7种高效布局方案全解析

戴森球计划工厂建设终极指南&#xff1a;7种高效布局方案全解析 【免费下载链接】FactoryBluePrints 游戏戴森球计划的**工厂**蓝图仓库 项目地址: https://gitcode.com/GitHub_Trending/fa/FactoryBluePrints 想要在戴森球计划中建造高效工厂却不知从何入手&#xff1f…

作者头像 李华
网站建设 2026/7/28 18:29:17

ms-swift支持清华镜像站加速pip包安装,提升环境初始化速度

ms-swift 支持清华镜像站加速 pip 包安装&#xff0c;提升环境初始化速度 在大模型研发日益工程化的今天&#xff0c;一个常见的“小问题”却常常成为项目启动的拦路虎&#xff1a;pip install 太慢了。 尤其对于中国大陆的开发者来说&#xff0c;从官方 PyPI 源安装 Python …

作者头像 李华
网站建设 2026/7/29 18:05:48

KitsuneMagisk完整使用教程:安卓Root权限与系统定制终极指南

KitsuneMagisk完整使用教程&#xff1a;安卓Root权限与系统定制终极指南 【免费下载链接】KitsuneMagisk A fork of KitsuneMagisk. Thanks to the original author HuskyDG. 项目地址: https://gitcode.com/gh_mirrors/ki/KitsuneMagisk 想要在安卓设备上安全获取Root权…

作者头像 李华
网站建设 2026/7/28 23:43:17

终极指南:如何在黑神话悟空中实现实时地图导航功能

终极指南&#xff1a;如何在黑神话悟空中实现实时地图导航功能 【免费下载链接】wukong-minimap 黑神话内置实时地图 / Black Myth: Wukong Built-in real-time map 项目地址: https://gitcode.com/gh_mirrors/wu/wukong-minimap 想要在《黑神话&#xff1a;悟空》中轻松…

作者头像 李华
网站建设 2026/7/28 14:39:30

ms-swift支持多实例并行训练加速实验迭代

ms-swift支持多实例并行训练加速实验迭代 在大模型研发日益成为AI竞争核心的今天&#xff0c;一个现实问题摆在每一个工程团队面前&#xff1a;如何在有限算力资源下&#xff0c;快速完成数十甚至上百次微调实验&#xff1f;传统做法是“排队等卡”&#xff0c;一个任务跑完再启…

作者头像 李华
网站建设 2026/7/28 18:39:23

graphql-request架构深度解析:从模块化设计到工程实践

graphql-request架构深度解析&#xff1a;从模块化设计到工程实践 【免费下载链接】graphql-request 项目地址: https://gitcode.com/gh_mirrors/gra/graphql-request GraphQL客户端库graphql-request以其简洁的API和强大的类型安全特性赢得了开发者的青睐。本文将从架…

作者头像 李华