Spring Retry
Overview
This article creates a simple project that uses Spring Retry with Kotlin.
Spring Retry provides a feature that automatically calls a specified method again when an exception occurs while calling a method.
It is useful for failover when a temporary network connection failure occurs.
Failover: alternative system operation.
This means building a non-stop system by keeping the server normally used and a cloned server, then letting the cloned server take over the work when the active server becomes difficult to use due to a failure.
Create the Project
Create the initial Spring Boot project with the following curl command.
curl https://start.spring.io/starter.tgz \
-d bootVersion=2.7.2 \
-d baseDir=spring-retry \
-d groupId=com.devkuma \
-d artifactId=spring-retry \
-d packageName=com.devkuma.retry \
-d applicationName=RetryApplication \
-d packaging=jar \
-d language=kotlin \
-d javaVersion=11 \
-d type=gradle-project | tar -xzvf -
Running this command creates a web project with Java 11 and Spring Boot 2.7.2.
Retry Configuration
Build Script
Add the libraries required to run Retry to the build script as follows.
/build.gradle.kts
dependencies {
// ... omitted ...
implementation("org.springframework.retry:spring-retry:1.3.3")
implementation("org.springframework:spring-aspects:5.3.22")
implementation("io.github.microutils:kotlin-logging:2.1.23")
// ... omitted ...
}
- spring-retry: Retry library
- spring-aspects: AOP-related library
- kotlin-logging: logging library for checking behavior
Add the Retry Configuration Class
Create a new RetryConfig configuration class and write it as follows.
/src/main/kotlin/com/devkuma/retry/config/RetryConfig.kt
package com.devkuma.retry.config
import org.springframework.context.annotation.Configuration
import org.springframework.retry.annotation.EnableRetry
@EnableRetry
@Configuration
class RetryConfig
Create Classes Related to Retry Application
Add the Service Interface and Class with Retry Applied
/src/main/kotlin/com/devkuma/retry/service/RetryService.kt
package com.devkuma.retry.service
import org.springframework.retry.annotation.Backoff
import org.springframework.retry.annotation.Retryable
import java.sql.SQLException
interface RetryService {
@Retryable(value = [SQLException::class], maxAttempts = 2, backoff = Backoff(delay = 1000))
fun retry(error: Boolean) : String
fun templateRetry(error: Boolean) : String
}
/src/main/kotlin/com/devkuma/retry/service/impl/RetryServiceImpl.kt
package com.devkuma.retry.service.impl
import com.devkuma.retry.service.RetryService
import mu.KotlinLogging
import org.springframework.retry.annotation.Recover
import java.sql.SQLException
private val log = KotlinLogging.logger {}
class RetryServiceImpl : RetryService {
override fun retry(error: Boolean) : String {
log.info { "Retry called. error=$error" }
if (error)
throw SQLException("retry SQLException")
return "Success"
}
@Recover
fun recover(exception: SQLException, error: Boolean) : String {
log.info { "Recover called: message=${exception.message}"}
return "Success"
}
}
- The
retryfunction declared with the@Retryableannotation retries when anSQLExceptionoccurs. It tries up to 2 retries with an interval of 1000 milliseconds. If the second attempt also fails, therecoverfunction configured with the@Recoverannotation is executed. - The
retryfunction raises an error when the argument variableerroristrue, and does not raise an error whenerrorisfalse.
Check with Retry Test Code
Add the Retry Test Code Class
Write the following test code to check the behavior.
/src/test/kotlin/com/devkuma/retry/service/RetryServiceTest.kt
package com.devkuma.retry.service
import com.devkuma.retry.config.RetryConfig
import com.devkuma.retry.service.impl.RetryServiceImpl
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest(
classes = [
RetryServiceImpl::class,
RetryConfig::class
]
)
class RetryServiceTest {
@Autowired
private lateinit var retryService: RetryService
@Test
fun `retry error=true`(){
// Given
// When
var result = retryService.retry(true)
// Then
Assertions.assertEquals("Success", result)
}
@Test
fun `retry error=false`(){
var result = retryService.retry(false)
Assertions.assertEquals("Success", result)
}
}
Check Retry Test Code Execution
When you run the test code, it behaves as follows.
When a Retry Error Occurs
The result when the retry error=true function is called is as follows.
2022-08-11 18:20:55.787 INFO 13102 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=true
2022-08-11 18:20:56.792 INFO 13102 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=true
2022-08-11 18:20:56.792 INFO 13102 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Recover called: message=retry SQLException, param error=true
When a Retry Error Does Not Occur
The result when the retry error=false function is called is as follows.
2022-08-11 18:20:56.801 INFO 13102 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=false
RetryTemplate Configuration
RetryOperations Interface
Spring Retry provides the RetryOperations interface, which offers a series of excute() methods.
package org.springframework.retry;
import org.springframework.retry.support.DefaultRetryState;
public interface RetryOperations {
// ... omitted ...
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback)
throws E;
// ... omitted ...
}
The RetryCallback parameter of execute() is an interface for inserting business logic that should be retried when it fails.
package org.springframework.retry;
public interface RetryCallback<T, E extends Throwable> {
T doWithRetry(RetryContext context) throws E;
}
If all retries fail, RetryOperations calls RecoveryCallback. To use this feature, the client must pass a RecoveryCallback object when calling the execute() method.
package org.springframework.retry;
public interface RecoveryCallback<T> {
T recover(RetryContext context) throws Exception;
}
RetryOperations Implementation
RetryTemplate is an implementation of RetryOperations. Configure a RetryTemplate Bean in the @Configuration class.
/src/main/kotlin/com/devkuma/retry/config/RetryConfig.kt
package com.devkuma.retry.config
import com.devkuma.retry.support.DefaultListenerSupport
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.retry.annotation.EnableRetry
import org.springframework.retry.backoff.FixedBackOffPolicy
import org.springframework.retry.policy.SimpleRetryPolicy
import org.springframework.retry.support.RetryTemplate
@EnableRetry
@Configuration
class RetryConfig {
@Bean
fun retryTemplate(): RetryTemplate {
val fixedBackOffPolicy = FixedBackOffPolicy()
fixedBackOffPolicy.backOffPeriod = 2000L
val retryPolicy = SimpleRetryPolicy()
retryPolicy.maxAttempts = 3
val retryTemplate = RetryTemplate()
retryTemplate.setBackOffPolicy(fixedBackOffPolicy)
retryTemplate.setRetryPolicy(retryPolicy)
retryTemplate.registerListener(DefaultListenerSupport())
return retryTemplate
}
}
RetryPolicyconfigures the number of retries for an operation.SimpleRetryPolicyis used to retry a fixed number of times.BackOffPolicyis used to control the backoff for retries.FixedBackOffPolicypauses for a fixed amount of time before continuing.
Check with RetryTemplate Test Code
Create the RetryTemplate Test Class
Only templateRetry was added to the existing test code.
/src/test/kotlin/com/devkuma/retry/service/RetryServiceTest.kt
package com.devkuma.retry.service
// ... omitted ...
class RetryServiceTest {
@Autowired
private lateinit var retryService: RetryService
@Autowired
private lateinit var retryTemplate: RetryTemplate
// ... omitted ...
@Test
fun templateRetry() {
retryTemplate.execute<Any, SQLException>(
{ retryService.templateRetry(true) },
{ retryService.templateRetry(false) }
)
}
}
Check RetryTemplate Test Execution
When TemplateRetry Does Not Produce an Error
The result when the templateRetry function is called is as follows.
2022-08-11 18:21:38.192 INFO 13197 --- [ Test worker] c.d.r.support.DefaultListenerSupport : onOpen
2022-08-11 18:21:41.384 INFO 13197 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=true
2022-08-11 18:21:42.389 INFO 13197 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Retry called. param error=true
2022-08-11 18:21:42.398 INFO 13197 --- [ Test worker] c.d.retry.service.impl.RetryServiceImpl : Recover called: message=retry SQLException, param error=true
2022-08-11 18:21:44.146 INFO 13197 --- [ Test worker] c.d.r.support.DefaultListenerSupport : onClose
References
- Spring Retry | Java Development Journal
- Guide to Spring Retry | Baeldung
- Spring Retry Review - 아빠프로그래머의 좌충우돌 개발하기!
- spring-projects/spring-retry | GitHub
The example code above is available on GitHub.