1. 概述

有时,为了提升应用性能和响应性,我们需要实现异步代码执行。此外,我们可能希望在遇到诸如网络故障等偶尔的异常时,自动重新调用代码。本教程将教你如何在Spring应用中实现异步执行与自动重试。

我们将探讨Spring对asyncretry操作的支持。

2. 示例Spring Boot应用

假设我们要构建一个简单的微服务,它调用下游服务处理一些数据。

2.1. Maven依赖项

首先,我们需要添加spring-boot-starter-web的Maven依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.2. 实现Spring服务

现在,我们来实现EventService类的方法,该方法调用另一个服务:

public String processEvents(List<String> events) {
    downstreamService.publishEvents(events);
    return "Completed";
}

接下来,定义DownstreamService接口:

public interface DownstreamService {
    boolean publishEvents(List<String> events);
}

3. 实现异步执行与重试

使用Spring的实现来实现异步执行与重试。

我们需要配置应用程序以支持asyncretry

3.1. 添加Retry Maven依赖

加入spring-retry到Maven依赖中:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.4</version>
</dependency>

3.2. @EnableAsync@EnableRetry配置

接下来,需要包含@EnableAsync@EnableRetry注解:

@Configuration
@ComponentScan("com.baeldung.asyncwithretry")
@EnableRetry
@EnableAsync
public class AsyncConfig {
}

3.3. 使用@Async@Retryable注解

要异步执行方法,我们需要使用@Async注解。同样,为重试执行方法添加@Retryable注解:

在上述EventService方法中配置这两个注解:

@Async
@Retryable(retryFor = RuntimeException.class, maxAttempts = 4, backoff = @Backoff(delay = 100))
public Future<String> processEvents(List<String> events) {
    LOGGER.info("Processing asynchronously with Thread {}", Thread.currentThread().getName());
    downstreamService.publishEvents(events);
    CompletableFuture<String> future = new CompletableFuture<>();
    future.complete("Completed");
    LOGGER.info("Completed async method with Thread {}", Thread.currentThread().getName());
    return future;
}

在这段代码中,我们在RuntimeException发生时重试方法,并返回一个Future对象。

请注意,我们应该使用FutureJava Future)包装来自任何异步方法的结果。

注意:@Async注解仅适用于公共方法,且不能在同一个类内部自我调用。如果自我调用,将跳过Spring代理调用,在同一线程中运行方法。

4. 测试@Async@Retryable

现在,让我们测试EventService方法,通过几个测试用例验证其异步和重试行为。

首先,当DownstreamService调用无误时,实现一个测试用例:

@Test
void givenAsyncMethodHasNoRuntimeException_whenAsyncMethodIscalled_thenReturnSuccess_WithoutAnyRetry() throws Exception {
    LOGGER.info("Testing for async with retry execution with thread " + Thread.currentThread().getName()); 
    when(downstreamService.publishEvents(anyList())).thenReturn(true);
    Future<String> resultFuture = eventService.processEvents(List.of("test1"));
    while (!resultFuture.isDone() && !resultFuture.isCancelled()) {
        TimeUnit.MILLISECONDS.sleep(5);
    }
    assertTrue(resultFuture.isDone());
    assertEquals("Completed", resultFuture.get());
    verify(downstreamService, times(1)).publishEvents(anyList());
}

在这个测试中,我们等待Future完成,然后进行断言。

接着,运行上述测试并检查日志输出:

18:59:24.064 [main] INFO com.baeldung.asyncwithretry.EventServiceIntegrationTest - Testing for async with retry execution with thread main
18:59:24.078 [SimpleAsyncTaskExecutor-1] INFO com.baeldung.asyncwithretry.EventService - Processing asynchronously with Thread SimpleAsyncTaskExecutor-1
18:59:24.080 [SimpleAsyncTaskExecutor-1] INFO com.baeldung.asyncwithretry.EventService - Completed async method with Thread SimpleAsyncTaskExecutor-1

从日志中,我们确认服务方法在单独的线程中运行。

接下来,我们实现一个DownstreamService方法抛出RuntimeException的测试用例:

@Test
void givenAsyncMethodHasRuntimeException_whenAsyncMethodIsCalled_thenReturnFailure_With_MultipleRetries() throws InterruptedException {
    LOGGER.info("Testing for async with retry execution with thread " + Thread.currentThread().getName()); 
    when(downstreamService.publishEvents(anyList())).thenThrow(RuntimeException.class);
    Future<String> resultFuture = eventService.processEvents(List.of("test1"));
    while (!resultFuture.isDone() && !resultFuture.isCancelled()) {
        TimeUnit.MILLISECONDS.sleep(5);
    }
    assertTrue(resultFuture.isDone());
    assertThrows(ExecutionException.class, resultFuture::get);
    verify(downstreamService, times(4)).publishEvents(anyList());
}

最后,验证上述测试用例的日志输出:

19:01:32.307 [main] INFO com.baeldung.asyncwithretry.EventServiceIntegrationTest - Testing for async with retry execution with thread main
19:01:32.318 [SimpleAsyncTaskExecutor-1] INFO com.baeldung.asyncwithretry.EventService - Processing asynchronously with Thread SimpleAsyncTaskExecutor-1
19:01:32.425 [SimpleAsyncTaskExecutor-1] INFO com.baeldung.asyncwithretry.EventService - Processing asynchronously with Thread SimpleAsyncTaskExecutor-1
.....

从日志中,我们确认服务方法被异步重试了四次。

5. 总结

在这篇文章中,我们学习了如何在Spring中实现带有重试机制的异步方法。我们在示例应用中实现了这一点,并尝试了几种情况,观察它如何处理不同的场景。我们了解了异步代码如何在独立线程上运行,并能自动重试。

如往常一样,示例代码可在GitHub上找到。


» 下一篇: Java Weekly, Issue 518