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
@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: 32.9 Test Results
After retries, the request eventually succeeds; if all retries fail, the fallback response "接口超时" is returned.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
