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.
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
<code>@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";
}</code>2.2 Aspect Definition
<code>@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;
}
}</code>2.3 Core timeoutAround Method
<code>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);
}</code>2.4 getExecutor Method
<code>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;
}</code>2.5 getFallbackMethod Method
<code>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;
}</code>2.6 resolveTimeout Method
<code>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);
}</code>2.7 Test Controller
<code>@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("接口超时");
}
}
</code>2.8 Configuration
<code>pack:
app:
api:
timeout: 3
</code>2.9 Test Results
After retries, the request eventually succeeds; if all retries fail, the fallback response "接口超时" is returned.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.