Master API Timeout in Spring Boot 3 with Custom @Timeout Annotation and AOP

This article demonstrates how to solve Spring Boot API timeout issues by creating a custom @Timeout annotation combined with an AOP aspect, covering annotation definition, aspect implementation, executor handling, fallback logic, configuration, and practical test results.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master API Timeout in Spring Boot 3 with Custom @Timeout Annotation and AOP

Environment: Spring Boot 3.4.2

1. Introduction

API timeout is a common performance and stability problem in Spring Boot applications. By using a custom annotation together with an AOP aspect, developers can configure timeout policies without writing repetitive code, improving maintainability and extensibility.

2. Practical Example

2.1 Custom Annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Timeout {
    // Base timeout (supports SpEL, e.g., "${pack.app.xxx.timeout}")
    String value() default "5000";
    // Time unit
    TimeUnit unit() default TimeUnit.MILLISECONDS;
    // Retry count (default no retry)
    int retry() default 0;
    // Retry interval (ms)
    long retryDelay() default 0;
    // Fallback method name (must be in the same class)
    String fallback() default "";
    // Executor bean name for thread pool
    String executor() default "timeoutExecutor";
}

2.2 Aspect Definition

@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 {
        // Core logic implemented in section 2.3
        return timeoutAround(pjp, timeout);
    }

    // Helper methods defined in sections 2.4‑2.6
    private Executor getExecutor(String executorBean) { /* ... */ }
    private Object handleFallback(ProceedingJoinPoint pjp, String fallbackMethod, Exception e) throws Exception { /* ... */ }
    private Method getFallbackMethod(ProceedingJoinPoint pjp, String fallback) { /* ... */ }
    private Object[] getParamValues(Throwable e, Method method, Object... args) { /* ... */ }
    private long resolveTimeout(Method method, Timeout timeout) { /* ... */ }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
}

2.3 Core timeoutAround Method

public Object timeoutAround(ProceedingJoinPoint pjp, Timeout timeout) throws Throwable {
    Method method = ((MethodSignature) pjp.getSignature()).getMethod();
    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("{} - call timed out, {}", method, e.getMessage());
            if (attempt++ >= retry) {
                return handleFallback(pjp, fallbackMethod, e);
            }
            long waitTime = timeout.retryDelay() * (long) Math.pow(2, attempt - 1);
            TimeUnit.MILLISECONDS.sleep(waitTime);
            logger.warn("Retry {} after {} ms", attempt, waitTime);
        } catch (Exception e) {
            throw e.getCause();
        }
    } while (true);
}

2.4 getExecutor Method

private Executor getExecutor(String executorBean) {
    Executor executor = null;
    if (StringUtils.hasLength(executorBean)) {
        try {
            executor = this.beanFactory.getBean(executorBean, ExecutorService.class);
        } catch (Exception e) {
            int core = Runtime.getRuntime().availableProcessors();
            executor = new ThreadPoolExecutor(core, core, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1024));
        }
    }
    return executor;
}

2.5 getFallbackMethod Method

private Method getFallbackMethod(ProceedingJoinPoint pjp, String fallback) {
    MethodSignature ms = (MethodSignature) pjp.getSignature();
    Method method = ms.getMethod();
    Class<?>[] paramTypes = method.getParameterTypes();
    Method fallbackMethod = null;
    try {
        fallbackMethod = method.getDeclaringClass().getDeclaredMethod(fallback, paramTypes);
        fallbackMethod.setAccessible(true);
    } catch (Exception e) {
        Class<?>[] types = new Class<?>[paramTypes.length + 1];
        System.arraycopy(paramTypes, 0, types, 0, paramTypes.length);
        types[types.length - 1] = Throwable.class;
        try {
            fallbackMethod = method.getDeclaringClass().getDeclaredMethod(fallback, types);
        } catch (Exception ex) {
            logger.error("Failed to obtain fallback method: {}", ex.getMessage());
        }
    }
    return fallbackMethod;
}

2.6 resolveTimeout Method

private long resolveTimeout(Method method, Timeout timeout) {
    String expr = timeout.value();
    DefaultListableBeanFactory bf = (DefaultListableBeanFactory) beanFactory;
    String embedded = bf.resolveEmbeddedValue(expr);
    Object value = bf.getBeanExpressionResolver().evaluate(embedded,
        new BeanExpressionContext(bf, null));
    Long ms = bf.getConversionService().convert(value, Long.class);
    return timeout.unit().toMillis(ms);
}

2.7 Test Controller

@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("接口超时");
    }
}

2.8 Configuration

pack:
  app:
    api:
      timeout: 3

2.9 Test Results

Test result 1
Test result 1
Test result 2
Test result 2
Test result 3
Test result 3

After retries, the request eventually succeeds; if all retries fail, the fallback response "接口超时" is returned.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javaaopSpring BootCustom AnnotationAPI timeout
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.