Search code examples
javaspringspring-bootmicroservicesspring-aop

How to implement application-level retry logic using Spring AOP without modifying service classes?


I want to implement a retry mechanism at the application level in a Spring Boot application. However, I don't want to add @Retryable or make any changes to the service methods or their classes. My goal is to implement the retry logic using Spring AOP and a centralized configuration, such as RetryTemplate, for consistency across the application.

Here’s what I have done so far:

Aspect Class I created an aspect that uses RetryTemplate to handle retries for all methods within @Service classes.

@Aspect
@Component
public class DbConnectionRetryAspect {

    private final RetryTemplate retryTemplate;

    public DbConnectionRetryAspect(RetryTemplate retryTemplate) {
        this.retryTemplate = retryTemplate;
    }

    @Around("@within(org.springframework.stereotype.Service)")
    public Object retry(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Aspect invoked for method: " + joinPoint.getSignature());

        return retryTemplate.execute(context -> {
            System.out.println("Retry attempt: " + context.getRetryCount());
            try {
                return joinPoint.proceed();
            } catch (SQLTransientConnectionException e) {
                System.out.println("Retryable exception caught: " + e.getMessage());
                throw e; // Ensure the exception propagates for retries
            }
        });
    }
}

RetryTemplate Configuration I created a RetryTemplate bean to define the retry logic.

@Bean
public RetryTemplate retryTemplate() {
    RetryTemplate retryTemplate = new RetryTemplate();

    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(
        5,  // maxAttempts
        Map.of(SQLTransientConnectionException.class, true)
    );
    retryTemplate.setRetryPolicy(retryPolicy);

    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
    backOffPolicy.setBackOffPeriod(1000); // 1 second
    retryTemplate.setBackOffPolicy(backOffPolicy);

    return retryTemplate;
}

Service Class The service method throws a SQLTransientConnectionException when the operation fails. I don’t want to annotate this method with @Retryable.

@Service
public class MyService {

    public void performOperation() throws SQLTransientConnectionException {
        System.out.println("Service method invoked");
        throw new SQLTransientConnectionException("Simulated transient error");
    }
}

Configuration I added the following configuration to enable AspectJ:

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AspectJConfig {
}

The Problem: While the aspect is being invoked, the retry logic is not working as expected. Specifically:

  • The RetryTemplate logs indicate that retries are attempted (Retry attempt: X), but the service method is only called once.

  • It seems the exception is not propagating correctly or the retry logic is not being applied to the service method.

What I Need Help With: How can I implement application-level retry logic using Spring AOP and RetryTemplate such that:

  • The retry mechanism works as expected.
  • No changes are required in the service classes or methods (e.g., no annotations like @Retryable).
  • The retry attempts and backoff policies are configurable via the centralized RetryTemplate.

Any guidance or suggestions would be greatly appreciated.

Thank you!


Solution

  • The issue was caused by the @Transactional annotation on my custom @CustomService annotation. Since @Transactional proxies the class to handle transaction management, it interfered with the retry mechanism when combined with @Service. The retry logic was being applied to the transactional proxy, which was likely causing the method invocation to bypass the retry logic after the first call.