Mastering Retry Strategies in Java: From Manual Loops to Spring Retry and Guava
This article walks through seven retry implementations for Java services—manual loops, static proxy, JDK dynamic proxy, CGLib proxy, custom AOP, Spring Retry annotations, and Guava‑retry—showing code examples, analyzing their pros and cons, and recommending the best fit for different project contexts.
1. Manual Retry
Using a while loop to repeat a remote call until it succeeds. The example deliberately throws an exception ( int i = 3 / 0;) and retries up to five times, sleeping two seconds between attempts.
@Service
public class OrderServiceImpl implements OrderService {
public void addOrder() {
int times = 1;
while (times <= 5) {
try {
// intentional exception
int i = 3 / 0;
// addOrder logic
} catch (Exception e) {
System.out.println("Retry " + times + " times");
Thread.sleep(2000);
times++;
if (times > 5) {
throw new RuntimeException("No more retries!");
}
}
}
}
}Drawbacks: no back‑off interval, high code intrusion, and duplication when many services need retries.
2. Static Proxy
Wrap the original service with a proxy class that contains the retry loop, leaving the business class untouched.
@Service
public class OrderServiceProxyImpl implements OrderService {
@Autowired
private OrderServiceImpl orderService;
@Override
public void addOrder() {
int times = 1;
while (times <= 5) {
try {
int i = 3 / 0;
orderService.addOrder();
} catch (Exception e) {
System.out.println("Retry " + times + " times");
Thread.sleep(2000);
times++;
if (times > 5) {
throw new RuntimeException("No more retries!");
}
}
}
}
}Improves separation but still requires a separate proxy for each service, which becomes cumbersome.
3. JDK Dynamic Proxy
Creates a proxy at runtime for any interface, centralising retry logic in an InvocationHandler.
public class RetryInvocationHandler implements InvocationHandler {
private final Object subject;
public RetryInvocationHandler(Object subject) { this.subject = subject; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int times = 1;
while (times <= 5) {
try {
int i = 3 / 0; // intentional exception
return method.invoke(subject, args);
} catch (Exception e) {
System.out.println("Retry【" + times + "】times");
Thread.sleep(2000);
times++;
if (times > 5) {
throw new RuntimeException("No more retries!");
}
}
}
return null;
}
public static Object getProxy(Object realSubject) {
InvocationHandler handler = new RetryInvocationHandler(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler);
}
}Usage in a controller:
@RestController
@RequestMapping("/order")
public class OrderController {
@Qualifier("orderServiceImpl")
@Autowired
private OrderService orderService;
@GetMapping("/addOrder")
public String addOrder() {
OrderService proxy = (OrderService) RetryInvocationHandler.getProxy(orderService);
proxy.addOrder();
return "addOrder";
}
}Limitation: the target class must implement an interface; otherwise the proxy cannot be created.
4. CGLib Dynamic Proxy
Uses byte‑code generation to subclass concrete classes, removing the interface requirement.
@Component
public class CGLibRetryProxyHandler implements MethodInterceptor {
private Object target;
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
int times = 1;
while (times <= 5) {
try {
int i = 3 / 0; // intentional exception
return method.invoke(target, args);
} catch (Exception e) {
System.out.println("Retry【" + times + "】times");
Thread.sleep(2000);
times++;
if (times > 5) {
throw new RuntimeException("No more retries!");
}
}
}
return null;
}
public Object getCglibProxy(Object objectTarget) {
this.target = objectTarget;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(objectTarget.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
}Controller usage:
@GetMapping("/addOrder")
public String addOrder() {
OrderService proxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService);
proxy.addOrder();
return "addOrder";
}Advantage: works for classes without interfaces, but still requires explicit proxy creation for each bean.
5. Manual AOP (Custom Annotation)
Define a @MyRetryable annotation and an aspect that intercepts methods annotated with it.
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency> @Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRetryable {
int retryTimes() default 3;
int retryInterval() default 1; // seconds
}
@Slf4j
@Aspect
@Component
public class RetryAspect {
@Pointcut("@annotation(com.hcr.sbes.retry.annotation.MyRetryable)")
private void retryMethodCall() {}
@Around("retryMethodCall()")
public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
MyRetryable retry = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(MyRetryable.class);
int maxRetryTimes = retry.retryTimes();
int retryInterval = retry.retryInterval();
Throwable error = new RuntimeException();
for (int i = 1; i <= maxRetryTimes; i++) {
try {
return joinPoint.proceed();
} catch (Throwable t) {
error = t;
log.warn("Exception occurred, retrying, retryTimes:{}", i);
Thread.sleep(retryInterval * 1000L);
}
}
throw new RuntimeException("Retry attempts exhausted", error);
}
}Apply to a service method:
@Service
public class OrderServiceImpl implements OrderService {
@Override
@MyRetryable(retryTimes = 5, retryInterval = 2)
public void addOrder() {
int i = 3 / 0; // intentional exception
}
}This eliminates duplicated retry code while keeping the business class clean.
6. Spring Retry
Add the Spring Retry starter and enable it with @EnableRetry. Use @Retryable on the target method and @Recover for fallback.
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency> @Service
public class OrderServiceImpl implements OrderService {
@Override
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2))
public void addOrder() {
System.out.println("Retrying...");
int i = 3 / 0;
}
@Recover
public void recover(RuntimeException e) {
log.error("Max retry attempts reached", e);
}
}Features: configurable max attempts, exponential back‑off, and a separate recovery method. Limitation: it only retries on exceptions; it cannot retry based on a returned value.
7. Guava‑Retrying
Guava’s Retryer offers fine‑grained control, including retry based on result values.
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency> public String guavaRetry(Integer num) {
Retryer<String> retryer = RetryerBuilder<String>newBuilder()
.retryIfException()
.retryIfResult(result -> Objects.equals(result, "error"))
.withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
System.out.println("RetryListener: attempt " + attempt.getAttemptNumber());
}
})
.build();
try {
return retryer.call(() -> testGuavaRetry(num));
} catch (Exception e) {
e.printStackTrace();
return "fallback";
}
}Advantages: can retry on specific exceptions, on undesired return values, and provides listeners for custom side‑effects. It works independently of Spring, making it suitable for non‑Spring projects.
Summary
For Spring‑based applications, Spring Retry offers the most concise annotation‑driven solution and handles most retry scenarios out of the box. In non‑Spring environments, Guava‑Retrying provides comparable flexibility with result‑based retry support. Manual loops, static proxies, and custom AOP are educational but introduce boiler‑plate and maintenance overhead; dynamic proxies (JDK or CGLib) centralise logic but still require explicit proxy creation. Choosing the right approach depends on the project’s framework stack, the need for result‑based retries, and how much boiler‑plate the team is willing to tolerate.
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
