对接第三方系统时,Token 过期是高频痛点。传统方案到处写if‑else判断 401、手动刷新 Token、手写重试逻辑,业务代码被非业务逻辑污染。本文介绍一款注解驱动的通用组件 asset‑util‑tokenRetry,通过 AOP 切面实现 Token 过期自动刷新 + 自动重试,做到业务代码零侵入。
一、背景痛点
在对接各类第三方外部系统(U9、财务共享、IMS 等)时,经常遇到 Token 过期场景:
- 调用接口返回 401 / 抛出异常代表 Token 失效;
- 业务代码到处充斥 Token 过期判断、手动调用刷新 token、手动重试;
- 每个对接系统都要重复写一套相同的重试样板代码,代码冗余;
- 刷新 Token 逻辑散落在各个 service,维护成本高,漏写就会出现线上报错。
理想目标:
只需要加一个注解,当检测 Token 过期,切面自动完成刷新 Token、自动重试,业务代码只关心本身业务逻辑。
基于这个诉求,我们封装了asset‑util‑tokenRetry通用工具包,基于 Spring AOP + SpringBoot 自动装配实现。
二、组件核心能力
✨ 注解驱动,非侵入式 :@TokenRetry注解标记目标方法,业务代码无侵入。 ✨ 多第三方系统兼容 :不同外部系统分别实现管理器,通过 system 标识隔离。 ✨ 双重过期识别机制 :支持异常抛出检测 + 接口返回值检测 两种模式识别 token 过期。 ✨ 自动装配 :引入 Maven 依赖,编写对应管理器 Bean 即可直接使用。 ✨ 可控重试次数:自定义最大重试次数,防止无限死循环。
三、快速上手使用
1. 引入 Maven 依赖
XML
<dependency>
<groupId>com.meide.asset</groupId>
<artifactId>asset-util-tokenRetry</artifactId>
<version>1.0.0</version>
</dependency>
2. 实现每个第三方系统的 TokenRetryManager
为每一个对接的外部系统,实现TokenRetryManager接口,并注册为 Spring Bean。
java
import org.springframework.stereotype.Component;
/**
* 财务共享系统token刷新管理器
*/
@Component
public class FinancialShareTokenRetryManager implements TokenRetryManager {
/**
* 系统唯一标识,和注解 @TokenRetry(system="xxx")对应
*/
@Override
public String getSystem() {
return "financialShare";
}
/**
* 【核心】刷新token逻辑:调用第三方获取token接口,存入缓存(Redis/内存)
*/
@Override
public void refreshToken() {
// 1.调用第三方token接口获取新token
String newToken = httpClient.get("https://xxx.com/api/token");
// 2.把新token存入redis缓存,业务方法直接从缓存读取
redisTemplate.opsForValue().set("financial_share:token", newToken);
}
/**
* 可选:从接口返回结果判断token是否过期
* 例如返回code=401代表token失效
*/
@Override
public boolean isTokenExpired(Object response) {
if (response instanceof ResponseDTO) {
return ((ResponseDTO) response).getCode() == 401;
}
return false;
}
}

- 业务 Service 添加注解使用
java
@Service
public class FinancialShareService {
/**
* system:对应管理器的系统标识
* maxRetries:最大重试次数(不包含首次调用,最多执行1+maxRetries次)
*/
@TokenRetry(system = "financialShare", maxRetries = 2)
public ResponseDTO queryData(Map<String, Object> params) {
// 直接从缓存拿token,不需要关心过期刷新
String token = getTokenFromCache();
ResponseDTO result = httpClient.post(url, token, params);
// 方式一:手动抛出TokenExpiredException触发重试
if (result.getCode() == 401) {
throw new TokenExpiredException("Token已过期");
}
return result;
}
}
💡两种触发重试方式二选一即可
- 异常触发 :业务代码抛出
TokenExpiredException; - 返回值触发 :在
TokenRetryManager#isTokenExpired识别返回对象,不需要业务抛异常。

五、整体执行流程
整体分为两阶段检测:异常检测 、返回值检测
- 执行被
@TokenRetry标记的业务方法; - AOP 切面根据 system 找到对应的
TokenRetryManager; - 执行业务方法:
- 如果抛出
TokenExpiredException:判断是否还有重试次数;有则调用refreshToken()刷新 token,重新执行业务方法;无则向外抛出异常; - 方法正常返回:调用
manager.isTokenExpired()判断返回结果是否 token 过期;true 则刷新 token 并重试;false 直接返回结果;
- 如果抛出
- 其他非 TokenExpiredException 异常直接向上抛出,不会触发重试逻辑。
注意:
void返回的方法,不会执行返回值检测,只能通过抛出 TokenExpiredException 来触发重试

六、关键注意事项(生产踩坑点)
1. 事务 @Transactional 的坑
如果业务方法加了@Transactional,重试依然在同一个事务内部执行。
refreshToken()只是刷新缓存 token,本身不操作数据库一般不受影响; 如果 refreshToken 内部包含数据库操作,同一个事务回滚会把刷新动作也回滚。 👉 解决方案:refreshToken 内 DB 操作使用独立新事务(REQUIRES_NEW)。
2. 并发场景
内部注册中心TokenRetryManagerRegistry使用ConcurrentHashMap,支持并发注册与查询,线程安全。
3. 重复注册 Manager
同一个system注册多个 Bean,后注册的 Bean 会覆盖前面的,同时输出 WARN 日志提醒。
4. 重试次数配置
maxRetries不要设置过大,建议 1‑2 次即可,避免死循环。
5. void 返回方法限制
void 方法不会执行返回值检测,只能靠抛TokenExpiredException触发重试。
6. 非 token 异常不会重试
只有两种情况触发重试:
- 抛出
TokenExpiredException; isTokenExpired(response)返回 true。
网络超时、业务报错等其他异常直接抛出,不重
九、总结
这套组件把 token 过期的识别、刷新、重试全部交给 AOP 切面处理,业务代码只需要添加注解,消除样板代码。 对于系统对接 U9、EBS、IMS 等多外部系统场景,非常适合接入。