diff --git a/spring-boot-modules/pom.xml b/spring-boot-modules/pom.xml index 655bcb67cf26..9aaaf8eac83f 100644 --- a/spring-boot-modules/pom.xml +++ b/spring-boot-modules/pom.xml @@ -116,6 +116,7 @@ spring-boot-3-4 spring-boot-4 spring-boot-resilience4j + spring-boot-retries spring-boot-properties spring-boot-properties-2 spring-boot-properties-3 diff --git a/spring-boot-modules/spring-boot-retries/pom.xml b/spring-boot-modules/spring-boot-retries/pom.xml new file mode 100644 index 000000000000..4fe8e29ceb2d --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + com.baeldung.spring-boot-retries + spring-boot-retries + 1.0.0-SNAPSHOT + spring-boot-retries + + + com.baeldung.spring-boot-modules + spring-boot-modules + 1.0.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.apache.httpcomponents.client5 + httpclient5 + 5.3.1 + + + org.apache.httpcomponents.core5 + httpcore5 + 5.2.1 + + + + org.springframework.retry + spring-retry + + + org.springframework + spring-aspects + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/RetrylogicApplication.java b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/RetrylogicApplication.java new file mode 100644 index 000000000000..2b642743c6f4 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/RetrylogicApplication.java @@ -0,0 +1,14 @@ +package com.baeldung.retries; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class RetrylogicApplication { + + public static void main(String[] args) { + SpringApplication.run(RetrylogicApplication.class, args); + } + +} + diff --git a/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/config/RestTemplateConfig.java b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/config/RestTemplateConfig.java new file mode 100644 index 000000000000..cfada2b85e7e --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/config/RestTemplateConfig.java @@ -0,0 +1,23 @@ +package com.baeldung.retries.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.retry.annotation.EnableRetry; +import org.springframework.web.client.RestTemplate; + +@Configuration +@EnableRetry +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + HttpComponentsClientHttpRequestFactory factory = + new HttpComponentsClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setConnectionRequestTimeout(5000); + + return new RestTemplate(factory); + } +} + diff --git a/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/config/RetryTemplateConfig.java b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/config/RetryTemplateConfig.java new file mode 100644 index 000000000000..2cb8c8c888c4 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/config/RetryTemplateConfig.java @@ -0,0 +1,28 @@ +package com.baeldung.retries.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.retry.backoff.FixedBackOffPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; + +@Configuration +public class RetryTemplateConfig { + + @Bean + public RetryTemplate retryTemplate() { + RetryTemplate retryTemplate = new RetryTemplate(); + + FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); + backOffPolicy.setBackOffPeriod(2000); + + SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); + retryPolicy.setMaxAttempts(3); + + retryTemplate.setBackOffPolicy(backOffPolicy); + retryTemplate.setRetryPolicy(retryPolicy); + + return retryTemplate; + } +} + diff --git a/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/controller/RetryController.java b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/controller/RetryController.java new file mode 100644 index 000000000000..8e18bd5e54b6 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/controller/RetryController.java @@ -0,0 +1,48 @@ +package com.baeldung.retries.controller; + +import com.baeldung.retries.service.ExponentialBackoffRetryService; +import com.baeldung.retries.service.RestTemplateRetryService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class RetryController { + + private final RestTemplateRetryService retryService; + private final ExponentialBackoffRetryService exponentialService; + + public RetryController(RestTemplateRetryService retryService, + ExponentialBackoffRetryService exponentialService) { + this.retryService = retryService; + this.exponentialService = exponentialService; + } + + @GetMapping("/fetch-with-retry") + public ResponseEntity fetchWithRetry(@RequestParam String url) { + try { + String result = retryService.makeRequestWithRetry(url); + return ResponseEntity.ok(result); + } catch (RuntimeException e) { + return ResponseEntity.status(503) + .body("Service unavailable after retries: " + e.getMessage()); + } + } + + @GetMapping("/fetch-with-exponential-backoff") + public ResponseEntity fetchWithExponentialBackoff( + @RequestParam String url) { + try { + String result = exponentialService + .makeRequestWithExponentialBackoff(url); + return ResponseEntity.ok(result); + } catch (RuntimeException e) { + return ResponseEntity.status(503) + .body("Service unavailable after retries: " + e.getMessage()); + } + } +} + diff --git a/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/ExponentialBackoffRetryService.java b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/ExponentialBackoffRetryService.java new file mode 100644 index 000000000000..7f74d5e368e6 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/ExponentialBackoffRetryService.java @@ -0,0 +1,49 @@ +package com.baeldung.retries.service; + +import org.springframework.stereotype.Service; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +@Service +public class ExponentialBackoffRetryService { + + private final RestTemplate restTemplate; + private int maxRetries = 5; + private long initialDelay = 1000; + + public ExponentialBackoffRetryService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public String makeRequestWithExponentialBackoff(String url) { + int attempt = 0; + while (attempt < maxRetries) { + try { + return restTemplate.getForObject(url, String.class); + } catch (ResourceAccessException e) { + attempt++; + if (attempt >= maxRetries) { + throw new RuntimeException( + "Failed after " + maxRetries + " attempts", e); + } + long delay = initialDelay * (long) Math.pow(2, attempt - 1); + try { + Thread.sleep(delay); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Retry interrupted", ie); + } + } + } + throw new RuntimeException("Unexpected error in retry logic"); + } + + public void setMaxRetries(int maxRetries) { + this.maxRetries = maxRetries; + } + + public void setInitialDelay(long initialDelay) { + this.initialDelay = initialDelay; + } +} + diff --git a/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/RestClientService.java b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/RestClientService.java new file mode 100644 index 000000000000..699e3353335c --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/RestClientService.java @@ -0,0 +1,33 @@ +package com.baeldung.retries.service; + +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.retry.annotation.Recover; +import org.springframework.stereotype.Service; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + + +@Service +public class RestClientService { + + private final RestTemplate restTemplate; + + public RestClientService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Retryable( + retryFor = {ResourceAccessException.class}, + maxAttempts = 3, + backoff = @Backoff(delay = 2000)) + public String fetchData(String url) { + return restTemplate.getForObject(url, String.class); + } + + @Recover + public String recover(ResourceAccessException e, String url) { + return "Fallback response after all retries failed for: " + url; + } +} + diff --git a/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/RestTemplateRetryService.java b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/RestTemplateRetryService.java new file mode 100644 index 000000000000..aa3ee4421cc3 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/RestTemplateRetryService.java @@ -0,0 +1,48 @@ +package com.baeldung.retries.service; + +import org.springframework.stereotype.Service; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +@Service +public class RestTemplateRetryService { + + private final RestTemplate restTemplate; + private int maxRetries = 3; + private long retryDelay = 2000; + + public RestTemplateRetryService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public String makeRequestWithRetry(String url) { + int attempt = 0; + while (attempt < maxRetries) { + try { + return restTemplate.getForObject(url, String.class); + } catch (ResourceAccessException e) { + attempt++; + if (attempt >= maxRetries) { + throw new RuntimeException( + "Failed after " + maxRetries + " attempts", e); + } + try { + Thread.sleep(retryDelay); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Retry interrupted", ie); + } + } + } + throw new RuntimeException("Unexpected error in retry logic"); + } + + public void setMaxRetries(int maxRetries) { + this.maxRetries = maxRetries; + } + + public void setRetryDelay(long retryDelay) { + this.retryDelay = retryDelay; + } +} + diff --git a/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/RetryTemplateService.java b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/RetryTemplateService.java new file mode 100644 index 000000000000..f12d82fdff17 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/main/java/com/baeldung/retries/service/RetryTemplateService.java @@ -0,0 +1,26 @@ +package com.baeldung.retries.service; + +import org.springframework.retry.support.RetryTemplate; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class RetryTemplateService { + + private final RestTemplate restTemplate; + private final RetryTemplate retryTemplate; + + public RetryTemplateService(RestTemplate restTemplate, RetryTemplate retryTemplate) { + this.restTemplate = restTemplate; + this.retryTemplate = retryTemplate; + } + + public String fetchDataWithRetryTemplate(String url) { + return retryTemplate.execute(context -> { + return restTemplate.getForObject(url, String.class); + }, context -> { + return "Fallback response"; + }); + } +} + diff --git a/spring-boot-modules/spring-boot-retries/src/main/resources/application.properties b/spring-boot-modules/spring-boot-retries/src/main/resources/application.properties new file mode 100644 index 000000000000..a1518926c186 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.application.name=retrylogic +server.port=8080 +logging.level.org.springframework.web.client=DEBUG diff --git a/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/retrylogic/RetrylogicApplicationTests.java b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/retrylogic/RetrylogicApplicationTests.java new file mode 100644 index 000000000000..ee84b7062ab9 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/retrylogic/RetrylogicApplicationTests.java @@ -0,0 +1,14 @@ +package com.baeldung.retries.retrylogic; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class RetrylogicApplicationTests { + + @Test + void contextLoads() { + } + +} + diff --git a/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/ExponentialBackoffRetryServiceTest.java b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/ExponentialBackoffRetryServiceTest.java new file mode 100644 index 000000000000..c4c7cbd90ac1 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/ExponentialBackoffRetryServiceTest.java @@ -0,0 +1,32 @@ +package com.baeldung.retries.service; + +import com.baeldung.retries.RetrylogicApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(classes = RetrylogicApplication.class) +class ExponentialBackoffRetryServiceTest { + + @Autowired + private ExponentialBackoffRetryService service; + + @Test + void whenHostOffline_thenRetriesWithExponentialBackoff() { + service.setMaxRetries(4); + service.setInitialDelay(500); + + String offlineUrl = "http://localhost:9999/api/data"; + long startTime = System.currentTimeMillis(); + + assertThrows(RuntimeException.class, () -> { + service.makeRequestWithExponentialBackoff(offlineUrl); + }); + + long duration = System.currentTimeMillis() - startTime; + assertTrue(duration >= 3500); + } +} + diff --git a/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/RestClientServiceTest.java b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/RestClientServiceTest.java new file mode 100644 index 000000000000..709b68d6bcfd --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/RestClientServiceTest.java @@ -0,0 +1,26 @@ +package com.baeldung.retries.service; + +import com.baeldung.retries.RetrylogicApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(classes = RetrylogicApplication.class) +class RestClientServiceTest { + + @Autowired + private RestClientService restClientService; + + @Test + void whenHostOffline_thenRetriesAndRecovers() { + String offlineUrl = "http://localhost:9999/api/data"; + + String result = restClientService.fetchData(offlineUrl); + + assertTrue(result.contains("Fallback response")); + assertTrue(result.contains(offlineUrl)); + } +} + diff --git a/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/RestTemplateRetryServiceTest.java b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/RestTemplateRetryServiceTest.java new file mode 100644 index 000000000000..d1a08a01fe79 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/RestTemplateRetryServiceTest.java @@ -0,0 +1,32 @@ +package com.baeldung.retries.service; + + +import com.baeldung.retries.RetrylogicApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(classes = RetrylogicApplication.class) +class RestTemplateRetryServiceTest { + + @Autowired + private RestTemplateRetryService service; + + @Test + void whenHostOffline_thenRetriesAndFails() { + String offlineUrl = "http://localhost:9999/api/data"; + + long startTime = System.currentTimeMillis(); + + assertThrows(RuntimeException.class, () -> { + service.makeRequestWithRetry(offlineUrl); + }); + + long duration = System.currentTimeMillis() - startTime; + assertTrue(duration >= 4000); + } + +} + diff --git a/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/RetryTemplateServiceTest.java b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/RetryTemplateServiceTest.java new file mode 100644 index 000000000000..30b8ec3ebcf2 --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/test/java/com/baeldung/retries/service/RetryTemplateServiceTest.java @@ -0,0 +1,35 @@ +package com.baeldung.retries.service; + +import com.baeldung.retries.RetrylogicApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@ContextConfiguration(classes = { + RetryTemplateService.class, + RetrylogicApplication .class +}) +class RetryTemplateServiceTest { + + @Autowired + private RetryTemplateService retryTemplateService; + + @Test + void whenHostOffline_thenReturnsFallback() { + String offlineUrl = "http://localhost:9999/api/data"; + + long startTime = System.currentTimeMillis(); + String result = retryTemplateService + .fetchDataWithRetryTemplate(offlineUrl); + long duration = System.currentTimeMillis() - startTime; + + assertEquals("Fallback response", result); + assertTrue(duration >= 4000); + } + +} + diff --git a/spring-boot-modules/spring-boot-retries/src/test/resources/application.properties b/spring-boot-modules/spring-boot-retries/src/test/resources/application.properties new file mode 100644 index 000000000000..907771133c8c --- /dev/null +++ b/spring-boot-modules/spring-boot-retries/src/test/resources/application.properties @@ -0,0 +1,3 @@ +server.port=8080 +logging.level.org.springframework.web.client=DEBUG +