In modern applications, transient failures (e.g., network timeouts, database connection issues, or external API unavailability) are inevitable. To build resilient systems, we need mechanisms to retry failed operations gracefully.
With Java 21 and Spring Boot 3, we can leverage Spring Retry to implement robust retry logic. In this post, I'll show you how to integrate Spring Retry into your application, complete with examples using virtual threads and asynchronous processing.
Why Use Spring Retry?
Spring Retry provides a declarative way to retry operations that may fail due to transient issues. Key features include:
- Retry Logic: Automatically retry failed operations with configurable attempts and backoff strategies.
- Fallback Mechanism: Define recovery logic when all retries fail.
- Integration with Spring: Seamlessly integrates with Spring Boot and other Spring components.
Key Concepts:
@Retryable:
Marks a method as retryable. You can specify the exceptions to retry, the maximum number of attempts, and the backoff strategy.
@Recover:
Defines a fallback method to execute when all retries fail. You can have multiple @Recover methods to handle different exceptions.
Why Multiple @Recover Methods?
Different exceptions may require different recovery logic. For example:
- A RuntimeException might require logging.
- An IOException might require returning a default response.
By defining multiple @Recover methods, you can handle each exception type appropriately.
Example 1: Retry with Virtual Threads
This example demonstrates how to retry an HTTP call using virtual threads.
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
@Service // This makes it a Spring-managed bean
public class VirtualThreadExample {
@Retryable(
retryFor = {RuntimeException.class, IOException.class, InterruptedException.class, ExecutionException.class, Exception.class},
maxAttempts = 4, // Total 4 attempts (1 initial + 3 retries)
backoff = @Backoff(delay = 1000, multiplier = 2)// Exponential backoff: 1s, 2s, 4s
)
public void getResponse(String urlRest) throws IOException {
System.out.println("Attempting to call: " + urlRest);
throw new RuntimeException("Negative Test cases for VirtualThreadExample");
}
@Recover
public void recover(RuntimeException e, String urlRest) {
System.err.println("All retries failed for URL: " + urlRest);
System.err.println("Error details: " + e.getMessage());
}
@Recover
public void recover(IOException e, String urlRest) {
System.err.println("All retries failed for URL: " + urlRest);
System.err.println("Error details: " + e.getMessage());
}
@Recover
public void recover(InterruptedException e, String urlRest) {
System.err.println("All retries failed for URL: " + urlRest);
System.err.println("Error details: " + e.getMessage());
}
@Recover
public void recover(ExecutionException e, String urlRest) {
System.err.println("All retries failed for URL: " + urlRest);
System.err.println("Error details: " + e.getMessage());
}
@Recover
public void recover(Exception e, String urlRest) {
System.err.println("All retries failed for URL: " + urlRest);
System.err.println("Error details: " + e.getMessage());
}
}
Expected Output:
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Processed 3 posts in 7060 millis
Program Completed !!
All retries failed for URL: https://jsonplaceholder.typicode.com/posts/3
Error details: Negative Test cases for VirtualThreadExample
All retries failed for URL: https://jsonplaceholder.typicode.com/posts/1
Error details: Negative Test cases for VirtualThreadExample
All retries failed for URL: https://jsonplaceholder.typicode.com/posts/2
Error details: Negative Test cases for VirtualThreadExample
Example 2: Retry with Asynchronous Processing
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class AsyncExample {
@Retryable(
retryFor = {RuntimeException.class}, // Retry on runtime exceptions
maxAttempts = 4, // Total 4 attempts (1 initial + 3 retries)
backoff = @Backoff(delay = 1000, multiplier = 2) // Exponential backoff: 1s, 2s, 4s
)
public void saveUser(String user) {
System.out.println("Saving user: " + user);
try {
Thread.sleep(1000); // Simulate database latency
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrupted while saving user", e);
}
// Simulate a transient failure
throw new RuntimeException("Failed to save user due to a transient error");
}
@Recover
public void recover(RuntimeException e, String user) {
System.err.println("All retries failed for user: " + user);
System.err.println("Error details: " + e.getMessage());
// Fallback logic (e.g., log the error, notify, or take corrective action)
}
}
Expected Output:
Saving user: JohnDoe
Saving user: JohnDoe
Saving user: JohnDoe
Saving user: JohnDoe
All retries failed for user: JohnDoe
Error details: Failed to save user due to a transient error
Running the Application
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.annotation.EnableRetry;
import java.util.List;
import java.util.concurrent.*;
@SpringBootApplication
@EnableRetry
public class DemoSpringRetryApplication {
public static void main(String[] args) {
SpringApplication.run(DemoSpringRetryApplication.class, args);
}
@Bean
ApplicationRunner asyncExampleRunner(AsyncExample example) {
return args -> {
String user = "JohnDoe";
// Run the task asynchronously
CompletableFuture.runAsync(() -> {
System.out.println("Starting async task for user: " + user);
example.saveUser(user); // Use the injected bean
System.out.println("Async task completed for user: " + user);
}, Executors.newVirtualThreadPerTaskExecutor())
.exceptionally(ex -> {
System.err.println("Failed to save user: " + user);
ex.printStackTrace();
return null;
});
System.out.println("Main thread continues executing...");
try {
Thread.sleep(2000); // Simulate main thread work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread finished.");
};
}
@Bean
ApplicationRunner virtualThreadExampleRunner(VirtualThreadExample example) {
return args -> {
try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
// List of posts to process
List<Integer> posts = List.of(1, 2, 3);
long start = System.nanoTime();
// Submit a task for each post
List<Future<Object>> futures = posts.stream()
.map(post -> myExecutor.submit(() -> {
example.getResponse("https://jsonplaceholder.typicode.com/posts/" + post);
return null; // Explicitly return null for Future<Void>
}))
.toList();
// Wait for all tasks to complete
for (Future<Object> future : futures) {
future.get(); // Ensures task completion
}
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.printf("Processed %d posts in %d millis%n", posts.size(), duration);
System.out.println("Program Completed !!");
} catch (InterruptedException | ExecutionException e) {
System.err.println("error " + e.getMessage());
}
};
}
}
Dependencies
<dependencies>
<!-- Spring Retry -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!-- Spring AOP (required for @EnableRetry) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
</dependencies>
Conclusion
Key Takeaways:
- Use @Retryable to define retry logic and @Recover for fallback behavior.
- Multiple @Recover methods allow you to handle different exceptions appropriately.
- Virtual threads and asynchronous processing improve concurrency and performance.
No comments:
Post a Comment