环境:SpringBoot3.4.2
1. 简介
在 Spring Boot 开发中,API 超时问题一直是影响系统性能与稳定性的常见困扰。它可能导致请求响应延迟,进而降低用户体验,甚至在极端情况下引发系统故障。然而,通过自定义注解 + AOP这一巧妙方式,我们可以轻松解决该问题。
开发者无需再为配置 API 超时策略而编写大量复杂代码,只需在目标方法上添加自定义注解(如:@Timeout),即可轻松实现超时时间的灵活设置。这一解决方案不仅简化了开发流程,提高了开发效率,还增强了系统的可维护性与扩展性,为 Spring Boot 应用的稳定运行提供了有力保障。
在之前的一篇文章中,我们已经介绍过比较完整的关于API超时的解决办法,详细请查看这篇文章:
虽然市面上解决接口超时相关的开源组件比较多(如:Resilience4j,非常推荐),但如你不愿引入第三方依赖增加复杂度,且为提升自身技术能力,那么本篇文章非常适合你。
最终,我们的效果如下:
@Timeout(
value = "${pack.app.api.timeout}",
unit = TimeUnit.SECONDS,
fallback = "fallbackQuery")
@GetMapping("/query")
public ResponseEntity<String> query() ;
// 同类中定义降级方法
public ResponseEntity<String> fallbackQuery(Throwable e)
接下来,我们将详细剖析该自定义注解的实现原理。
2. 实战案例
2.1 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Timeout {
// 基础超时时间(支持 SpEL 表达式,如 "${pack.app.xxx.timeout}")
String value() default "5000";
// 时间单位
TimeUnit unit() default TimeUnit.MILLISECONDS ;
// 重试次数(默认不重试)
int retry() default 0;
// 重试间隔(毫秒)
long retryDelay() default 0 ;
// 降级方法名(需在同一类中)
String fallback() default "";
// 线程池名称(指向配置的线程池 Bean)
String executor() default "timeoutExecutor";
}
每个属性已经进行注释说明了;
2.2 切面定义
@Aspect
@Component
public class TimeoutAspect implements BeanFactoryAware {
private static final Logger logger = LoggerFactory.getLogger(TimeoutAspect.class);
private BeanFactory beanFactory;
// 切面核心逻辑
@Around("@annotation(timeout)")
public Object timeoutAround(ProceedingJoinPoint pjp, Timeout timeout) throws Throwable {
// ...
}
// 获取线程池对象bean
private Executor getExecutor(String executorBean) {
// ...
}
// 调用降级方法(当发放执行超时时)
private Object handleFallback(ProceedingJoinPoint pjp, String fallbackMethod, Exception e) throws Exception {
try {
Method method = getFallbackMethod(pjp, fallbackMethod) ;
if (method == null) {
logger.error("{}", e) ;
return e.getMessage() ;
}
return method.invoke(pjp.getTarget(), getParamValues(e, method, pjp.getArgs()));
} catch (Exception ex) {
throw new TimeoutFallbackException("Fallback method not found: " + fallbackMethod, ex);
}
}
// 解析降级方法
private Method getFallbackMethod(ProceedingJoinPoint pjp, String fallback) {
// ...
}
// 解析执行的业务方法参数;同时会在最后拼如一个异常参数
private Object[] getParamValues(Throwable e, Method method, Object... args) {
int count = method.getParameterCount() ;
Object[] params = args ;
int len = args.length;
if (count == len + 1) {
params = new Object[count] ;
for (int i = 0; i < len; i++) {
params[i] = args[i] ;
}
params[count - 1] = e ;
}
return params;
}
// 解析 SpEL 表达式获取动态超时时间
private long resolveTimeout(Method method, Timeout timeout) {
// ...
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory ;
}
}
接下来,我们将详细的介绍上面的每一个方法。
切面核心timeoutAround方法
public Object timeoutAround(ProceedingJoinPoint pjp, Timeout timeout) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
// 根据配置注解上的超时属性value,解析SpEL表达式
long timeoutMs = resolveTimeout(method, timeout);
// 获取重试次数
int retry = timeout.retry();
// 获取降级方法名称
String fallbackMethod = timeout.fallback();
// 获取指定线程池
Executor executor = getExecutor(timeout.executor());
// 进入重试逻辑
int attempt = 0;
do {
try {
// 通过线程池对象提交有返回值的任务
Future<Object> future = ((ExecutorService) executor).submit(() -> {
try {
return pjp.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
});
// 获取结果数据时,设置超时时间
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// 只有发生超时异常时才会进入到重试的逻辑
logger.warn("{} - 调用超时, {}", method, e.getMessage()) ;
// 超过重试的次数后,调用降级方法
if (attempt++ >= retry) {
return handleFallback(pjp, fallbackMethod, e);
}
long pow = (long) Math.pow(2, attempt - 1) ;
long waitTime = timeout.retryDelay() * pow ;
TimeUnit.MILLISECONDS.sleep(waitTime) ;
logger.warn("第 {} 次, 重试 - {}", attempt, waitTime) ;
} catch (Exception e) {
throw e.getCause() ;
}
} while (true) ;
}
获取线程池getExecutor方法
private Executor getExecutor(String executorBean) {
Executor executor = null ;
if (StringUtils.hasLength(executorBean)) {
try {
// 根据注解@Timeout中配置的线程池beanName获取线程池对象
executor = this.beanFactory.getBean(executorBean, ExecutorService.class) ;
} catch (Exception e) {
// 无法获取bean时,创建默认的线程池对象,核心线程为当前CPU的核数
int core = Runtime.getRuntime().availableProcessors() ;
executor = new ThreadPoolExecutor(core, core, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1024)) ;
}
}
return executor ;
}
解析降级getFallbackMethod方法
private Method getFallbackMethod(ProceedingJoinPoint pjp, String fallback) {
MethodSignature ms = (MethodSignature) pjp.getSignature() ;
Method method = ms.getMethod();
Class<?>[] parameterTypes = method.getParameterTypes() ;
Method fallbackMethod = null ;
try {
// 获取与业务方法完全一样参数的降级方法
fallbackMethod = method.getDeclaringClass().getDeclaredMethod(fallback, parameterTypes) ;
fallbackMethod.setAccessible(true) ;
} catch (Exception e) {
// 获取不到方法,则获取最后一个参数带有异常对象的方法
int leng = parameterTypes.length;
Class<?>[] types = new Class<?>[leng + 1] ;
for (int i = 0; i < leng; i++) {
types[i] = parameterTypes[i] ;
}
types[leng] = Throwable.class ;
try {
fallbackMethod = method.getDeclaringClass().getDeclaredMethod(fallback, types) ;
} catch (Exception ex) {
logger.error("获取降级方法错误: {}", ex.getMessage()) ;
}
}
return fallbackMethod;
}
解析超时时间resolveTimeout方法
private long resolveTimeout(Method method, Timeout timeout) {
String timeoutExpr = timeout.value();
DefaultListableBeanFactory bf = (DefaultListableBeanFactory) beanFactory ;
String embeddedValue = bf.resolveEmbeddedValue(timeoutExpr) ;
Object value = bf.getBeanExpressionResolver().evaluate(embeddedValue, new BeanExpressionContext(bf, null)) ;
Long tm = bf.getConversionService().convert(value, Long.class) ;
return timeout.unit().toMillis(tm) ;
}
在该方法中我们完全使用的是Spring底层处理@Value注解的方式进行处理。
以上我们就完成了整个切面的代码。接下来,进行测试。
2.3 测试
@RestController
@RequestMapping("/api")
public class ApiController {
@Timeout(
value = "${pack.app.api.timeout}",
unit = TimeUnit.SECONDS,
fallback = "fallbackQuery",
retry = 3,
retryDelay = 3000)
@GetMapping("/query")
public ResponseEntity<String> query() throws Throwable {
TimeUnit.SECONDS.sleep(new Random().nextInt(6)) ;
return ResponseEntity.ok("success") ;
}
public ResponseEntity<String> fallbackQuery(Throwable e) {
return ResponseEntity.ok("接口超时") ;
}
}
配置文件配置超时时间
pack:
app:
api:
timeout: 3
测试结果

![]()
经过重试后成功。


三次重试都失败。
以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏
推荐文章
别懵圈啦!Spring Boot 这 8 个基础开发技能,你 get 到了没?
绝了!Spring Boot凭@JsonView注解,强大到逆天
高级开发!Spring Boot自定义注解实现接口动态切换,非常实用
非常实用!玩转 Spring Boot 接口参数类型转换,支持任意场景
技术专家:零代码,Spring Boot存储加密解密,支持JDBC、MyBatis及JPA
Spring Boot中记录JDBC、JPA及MyBatis执行SQL及参数的正确姿势
请不要自己写!Spring Boot 一个注解搞定逻辑删除,支持JPA/MyBatis
Spring Boot 3太强:全新Controller接口定义方式
我100%确定,你对@ComponentScan注解的了解仅限于皮毛









3万+

被折叠的 条评论
为什么被折叠?



