Backend Development 16 min read

Implementing Retry Mechanisms in Java: Manual Loops, Static Proxy, JDK Dynamic Proxy, CGLib Proxy, AOP, Spring Retry and Guava Retry

This article explains why retry mechanisms are essential for remote service calls in Java applications and demonstrates six practical implementations—from simple while‑loop retries to static proxies, JDK and CGLib dynamic proxies, custom AOP, Spring Retry annotations, and the Guava‑retry library—complete with code examples and usage tips.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing Retry Mechanisms in Java: Manual Loops, Static Proxy, JDK Dynamic Proxy, CGLib Proxy, AOP, Spring Retry and Guava Retry

As internet services become more complex, remote calls to third‑party APIs or internal services can fail due to network instability or server errors; adding a retry mechanism improves system robustness and stability.

1. Manual Retry (while loop)

Using a simple @Service public class OrderServiceImpl implements OrderService { public void addOrder() { int times = 1; while (times <= 5) { try { // intentionally cause exception int i = 3 / 0; // addOrder logic } catch (Exception e) { System.out.println("重试" + times + "次"); Thread.sleep(2000); times++; if (times > 5) { throw new RuntimeException("不再重试!"); } } } } } demonstrates the basic idea but has drawbacks such as lack of delay handling, high code intrusion, and duplication across many services.

2. Static Proxy

Encapsulating retry logic in a separate proxy class reduces intrusion:

@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("重试" + times + "次");
                try { Thread.sleep(2000); } catch (InterruptedException ex) { ex.printStackTrace(); }
                times++;
                if (times > 5) {
                    throw new RuntimeException("不再重试!");
                }
            }
        }
    }
}

While more elegant than manual code, creating a proxy for each service can become cumbersome.

3. JDK Dynamic Proxy

Using a single invocation handler to apply retry to any interface implementation:

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;
                return method.invoke(subject, args);
            } catch (Exception e) {
                System.out.println("重试【" + times + "】次");
                try { Thread.sleep(2000); } catch (InterruptedException ex) { ex.printStackTrace(); }
                times++;
                if (times > 5) { throw new RuntimeException("不再重试!"); }
            }
        }
        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 orderServiceProxy = (OrderService) RetryInvocationHandler.getProxy(orderService);
        orderServiceProxy.addOrder();
        return "addOrder";
    }
}

JDK proxies require the target class to implement an interface.

4. CGLib Dynamic Proxy

CGLib can proxy concrete classes without interfaces:

@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;
                return method.invoke(target, args);
            } catch (Exception e) {
                System.out.println("重试【" + times + "】次");
                try { Thread.sleep(2000); } catch (InterruptedException ex) { ex.printStackTrace(); }
                times++;
                if (times > 5) { throw new RuntimeException("不再重试!"); }
            }
        }
        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 orderServiceProxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService);
    orderServiceProxy.addOrder();
    return "addOrder";
}

5. Manual AOP (custom annotation)

Define a @MyRetryable annotation and a RetryAspect that intercepts annotated methods, applying configurable retry count and interval:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRetryable {
    int retryTimes() default 3;
    int retryInterval() default 1;
}

@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("调用发生异常,开始重试,retryTimes:{}", i); }
            Thread.sleep(retryInterval * 1000L);
        }
        throw new RuntimeException("重试次数耗尽", 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;
    }
}

6. Spring‑Retry

Add the dependency and enable retry with @EnableRetry . Annotate methods with @Retryable and optionally provide a @Recover method:

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
    @Override
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2))
    public void addOrder() {
        System.out.println("重试...");
        int i = 3 / 0;
    }
    @Recover
    public void recover(RuntimeException e) {
        log.error("达到最大重试次数", e);
    }
}

Spring‑Retry also offers many policies (NeverRetryPolicy, AlwaysRetryPolicy, SimpleRetryPolicy, etc.) for fine‑grained control.

7. Guava‑Retry

Guava‑retry provides flexible retry based on exceptions or return values:

@Override
public String guavaRetry(Integer num) {
    Retryer
retryer = RetryerBuilder.
newBuilder()
        .retryIfException()
        .retryIfResult(r -> Objects.equals(r, "error"))
        .withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))
        .withStopStrategy(StopStrategies.stopAfterAttempt(3))
        .withRetryListener(new RetryListener() {
            @Override
            public
void onRetry(Attempt
attempt) {
                System.out.println("RetryListener: 第" + attempt.getAttemptNumber() + "次调用");
            }
        })
        .build();
    try { retryer.call(() -> testGuavaRetry(num)); } catch (Exception e) { e.printStackTrace(); }
    return "test";
}

Guava‑retry can also be used directly with RetryTemplate for non‑Spring projects.

Conclusion

From manual loops to Spring annotations and Guava‑retry, the presented techniques cover most retry scenarios. Spring projects should prefer Spring‑Retry, while non‑Spring Java applications can adopt Guava‑retry for a lightweight yet powerful solution.

BackendJavaproxyaopSpringretryGuava
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.