news 2026/4/23 20:20:50

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/22 9:20:16

DataEase深度实战:重构企业数据决策的智能引擎

DataEase深度实战&#xff1a;重构企业数据决策的智能引擎 【免费下载链接】dataease DataEase: 是一个开源的数据可视化分析工具&#xff0c;支持多种数据源以及丰富的图表类型。适合数据分析师和数据科学家快速创建数据可视化报表。 项目地址: https://gitcode.com/GitHub_…

作者头像 李华
网站建设 2026/4/23 2:58:28

基于Taichi框架的声波传播高效仿真与可视化实践

基于Taichi框架的声波传播高效仿真与可视化实践 【免费下载链接】taichi Productive & portable high-performance programming in Python. 项目地址: https://gitcode.com/GitHub_Trending/ta/taichi 在现代计算物理和工程仿真领域&#xff0c;声波传播模拟一直是研…

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

终极cglib实战指南:从入门到精通的高效应用技巧

终极cglib实战指南&#xff1a;从入门到精通的高效应用技巧 【免费下载链接】cglib cglib - Byte Code Generation Library is high level API to generate and transform Java byte code. It is used by AOP, testing, data access frameworks to generate dynamic proxy obje…

作者头像 李华
网站建设 2026/4/21 0:08:23

PointMLP终极指南:如何用简约MLP架构重塑三维视觉格局

PointMLP终极指南&#xff1a;如何用简约MLP架构重塑三维视觉格局 【免费下载链接】pointMLP-pytorch [ICLR 2022 poster] Official PyTorch implementation of "Rethinking Network Design and Local Geometry in Point Cloud: A Simple Residual MLP Framework" …

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

在机器学习项目中利用 Python 继承

原文&#xff1a;towardsdatascience.com/leverage-python-inheritance-in-ml-projects-52e7e16401ab 简介 许多初涉机器学习的人没有强大的计算机工程背景&#xff0c;当他们需要在一个真实产品上工作时&#xff0c;他们的代码可能会很混乱&#xff0c;难以管理。这就是为什么…

作者头像 李华
网站建设 2026/4/19 7:48:53

CreamApi终极指南:免费解锁三大平台DLC的完整方案

CreamApi终极指南&#xff1a;免费解锁三大平台DLC的完整方案 【免费下载链接】CreamApi 项目地址: https://gitcode.com/gh_mirrors/cr/CreamApi 还在为心仪的DLC内容望而却步吗&#xff1f;CreamApi为你带来了革命性的解决方案&#xff01;&#x1f680; 这款强大的开…

作者头像 李华