哇塞!Spring Boot 一注解逆天,API 超时问题秒解决

环境:SpringBoot3.4.2



1. 简介

在 Spring Boot 开发中,API 超时问题一直是影响系统性能与稳定性的常见困扰。它可能导致请求响应延迟,进而降低用户体验,甚至在极端情况下引发系统故障。然而,通过自定义注解 + AOP这一巧妙方式,我们可以轻松解决该问题。

开发者无需再为配置 API 超时策略而编写大量复杂代码,只需在目标方法上添加自定义注解(如:@Timeout),即可轻松实现超时时间的灵活设置。这一解决方案不仅简化了开发流程,提高了开发效率,还增强了系统的可维护性与扩展性,为 Spring Boot 应用的稳定运行提供了有力保障。

在之前的一篇文章中,我们已经介绍过比较完整的关于API超时的解决办法,详细请查看这篇文章:

5种实现方式配置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 AI+MCP 三步实现智能体开发

太强了!Spring AI调用本地函数,实时获取最新数据

请不要自己写!Spring Boot 一个注解搞定逻辑删除,支持JPA/MyBatis

Spring Boot 3太强:全新Controller接口定义方式

七大陷阱!99%的Java开发者都会遇到

我100%确定,你对@ComponentScan注解的了解仅限于皮毛

​​​​​​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值