Spring AOP 实现通用 Token 自动刷新重试工具包

对接第三方系统时,Token 过期是高频痛点。传统方案到处写if‑else判断 401、手动刷新 Token、手写重试逻辑,业务代码被非业务逻辑污染。本文介绍一款注解驱动的通用组件 asset‑util‑tokenRetry,通过 AOP 切面实现 Token 过期自动刷新 + 自动重试,做到业务代码零侵入。

一、背景痛点

在对接各类第三方外部系统(U9、财务共享、IMS 等)时,经常遇到 Token 过期场景:

  1. 调用接口返回 401 / 抛出异常代表 Token 失效;
  2. 业务代码到处充斥 Token 过期判断、手动调用刷新 token、手动重试;
  3. 每个对接系统都要重复写一套相同的重试样板代码,代码冗余;
  4. 刷新 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;
    }
}
  1. 业务 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;
    }
}

💡两种触发重试方式二选一即可

  1. 异常触发 :业务代码抛出 TokenExpiredException
  2. 返回值触发 :在TokenRetryManager#isTokenExpired识别返回对象,不需要业务抛异常。

五、整体执行流程

整体分为两阶段检测:异常检测返回值检测

  1. 执行被@TokenRetry标记的业务方法;
  2. AOP 切面根据 system 找到对应的TokenRetryManager
  3. 执行业务方法:
    • 如果抛出TokenExpiredException:判断是否还有重试次数;有则调用refreshToken()刷新 token,重新执行业务方法;无则向外抛出异常;
    • 方法正常返回:调用manager.isTokenExpired()判断返回结果是否 token 过期;true 则刷新 token 并重试;false 直接返回结果;
  4. 其他非 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 异常不会重试

只有两种情况触发重试:

  1. 抛出TokenExpiredException
  2. isTokenExpired(response)返回 true。

网络超时、业务报错等其他异常直接抛出,不重

九、总结

这套组件把 token 过期的识别、刷新、重试全部交给 AOP 切面处理,业务代码只需要添加注解,消除样板代码。 对于系统对接 U9、EBS、IMS 等多外部系统场景,非常适合接入。

相关推荐
凤山老林20 分钟前
Spring Boot 3 + Spring Authorization Server 构建企业级 SSO:多端互通、会话共享与
spring boot·后端·spring·单点登录·sso·多端互通
Lenyiin20 分钟前
第6篇_Python高级语法与标准库精髓:从会用到达精通
java·python·html
YHHLAI23 分钟前
TypeScript 面试题:type 与 interface 的区别与相同点
java·ubuntu·typescript
马优晨24 分钟前
Spring Servlet 容器是什么、干什么用?
spring·servlet·内嵌servlet容器·java的内嵌servlet·spring内嵌servlet
咖啡八杯28 分钟前
PageHelper 分页封装:TableDataInfo 与 startPage() 的工作原理
java·spring boot·分页·若依·开源框架·pagehelper
多敲代码防脱发30 分钟前
Alibaba Sentinel(熔断、限流)(国内下载Sentinel)
java·开发语言·sentinel
.Hypocritical.33 分钟前
【SpringBoot】配置文件加载位置与优先级详解
java·spring boot·后端
JAVA面经实录9171 小时前
RabbitMQ 完整面试题答案
java·rabbitmq·java-rabbitmq